diff --git a/.github/ISSUE_TEMPLATE/auth-problem.md b/.github/ISSUE_TEMPLATE/auth-problem.md index bb394b3ad..25d2025d9 100644 --- a/.github/ISSUE_TEMPLATE/auth-problem.md +++ b/.github/ISSUE_TEMPLATE/auth-problem.md @@ -8,7 +8,7 @@ assignees: '' **Which version of GCM are you using?** -From a terminal, run `git credential-manager-core --version` and paste the output. +From a terminal, run `git credential-manager --version` and paste the output. - - - -### Linux - -#### Ubuntu/Debian distributions - -Download the latest [.deb package](https://github.com/GitCredentialManager/git-credential-manager/releases/latest), and run the following: - -```shell -sudo dpkg -i -git-credential-manager-core configure -``` - -**Note:** Although packages were previously offered on certain -[Microsoft Ubuntu package feeds](https://packages.microsoft.com/repos/), -GCM no longer publishes to these repositories. Please install the -Debian package using the above instructions instead. - -To uninstall: - -```shell -git-credential-manager-core unconfigure -sudo dpkg -r gcmcore -``` - -#### Other distributions - -##### Option 1: Tarball - -Download the latest [tarball](https://github.com/GitCredentialManager/git-credential-manager/releases/latest), and run the following: - -```shell -tar -xvf -C /usr/local/bin -git-credential-manager-core configure -``` - -To uninstall: - -```shell -git-credential-manager-core unconfigure -rm $(command -v git-credential-manager-core) -``` - -#### Option 2: Install from source helper script - -1. Ensure `curl` is installed: - - ```shell - curl --version - ``` - - If `curl` is not installed, please use your distribution's package manager - to install it. - -1. Download and run the script: - - ```shell - curl -LO https://raw.githubusercontent.com/GitCredentialManager/git-credential-manager/main/src/linux/Packaging.Linux/install-from-source.sh && - sh ./install-from-source.sh && - git-credential-manager-core configure - ``` - - **Note:** You will be prompted to enter your credentials so that the script - can download GCM's dependencies using your distribution's package - manager. - -To uninstall: - -[Follow these instructions](docs/linux-fromsrc-uninstall.md) for your distribution. - -**Note:** all Linux distributions [require additional configuration](https://aka.ms/gcm/credstores) to use GCM. - ---- - -### Windows - -GCM is included with [Git for Windows](https://gitforwindows.org/), and the latest version is included in each new Git for Windows release. This is the preferred way to install GCM on Windows. During installation you will be asked to select a credential helper, with GCM being set as the default. - -![image](https://user-images.githubusercontent.com/5658207/140082529-1ac133c1-0922-4a24-af03-067e27b3988b.png) - -#### Standalone installation - -You can also download the [latest installer](https://github.com/GitCredentialManager/git-credential-manager/releases/latest) for Windows to install GCM standalone. - -**:warning: Important :warning:** - -Installing GCM as a standalone package on Windows will forcibly override the version of GCM that is bundled with Git for Windows, **even if the version bundled with Git for Windows is a later version**. - -There are two flavors of standalone installation on Windows: - -- User (preferred) (`gcmuser-win*`): - - Does not require administrator rights. Will install only for the current user and updates only the current user's Git configuration. - -- System (`gcm-win*`): - - Requires administrator rights. Will install for all users on the system and update the system-wide Git configuration. - -To install, double-click the desired installation package and follow the instructions presented. - -#### Uninstall (Windows 10) - -To uninstall, open the Settings app and navigate to the Apps section. Select "Git Credential Manager" and click "Uninstall". - -#### Uninstall (Windows 7-8.1) - -To uninstall, open Control Panel and navigate to the Programs and Features screen. Select "Git Credential Manager" and click "Remove". - -#### Windows Subsystem for Linux (WSL) - -Git Credential Manager can be used with the [Windows Subsystem for Linux -(WSL)](https://aka.ms/wsl) to enable secure authentication of your remote Git -repositories from inside of WSL. - -[Please see the GCM on WSL docs](docs/wsl.md) for more information. - ## Supported Git versions Git Credential Manager tries to be compatible with the broadest set of Git @@ -218,50 +71,72 @@ Git that are not compatible. - Git 2.26.2 This version of Git introduced a breaking change with parsing credential - configuration that GCM relies on. This issue was fixed in commit [`12294990`](https://github.com/git/git/commit/12294990c90e043862be9eb7eb22c3784b526340) - of the Git project, and released in Git 2.27.0. + configuration that GCM relies on. This issue was fixed in commit + [`12294990`][gcm-commit-12294990] of the Git project, and released in Git + 2.27.0. ## How to use -Once it's installed and configured, Git Credential Manager is called implicitly by Git. -You don't have to do anything special, and GCM isn't intended to be called directly by the user. -For example, when pushing (`git push`) to [Azure DevOps](https://dev.azure.com), [Bitbucket](https://bitbucket.org), or [GitHub](https://github.com), a window will automatically open and walk you through the sign-in process. -(This process will look slightly different for each Git host, and even in some cases, whether you've connected to an on-premises or cloud-hosted Git host.) -Later Git commands in the same repository will re-use existing credentials or tokens that GCM has stored for as long as they're valid. +Once it's installed and configured, Git Credential Manager is called implicitly +by Git. You don't have to do anything special, and GCM isn't intended to be +called directly by the user. For example, when pushing (`git push`) to +[Azure DevOps][azure-devops], [Bitbucket][bitbucket], or [GitHub][github], a +window will automatically open and walk you through the sign-in process. (This +process will look slightly different for each Git host, and even in some cases, +whether you've connected to an on-premises or cloud-hosted Git host.) Later Git +commands in the same repository will re-use existing credentials or tokens that +GCM has stored for as long as they're valid. -Read full command line usage [here](docs/usage.md). +Read full command line usage [here][gcm-usage]. ### Configuring a proxy -See detailed information [here](https://aka.ms/gcm/httpproxy). +See detailed information [here][gcm-http-proxy]. ## Additional Resources -- [Frequently asked questions](docs/faq.md) -- [Development and debugging](docs/development.md) -- [Command-line usage](docs/usage.md) -- [Configuration options](docs/configuration.md) -- [Environment variables](docs/environment.md) -- [Enterprise configuration](docs/enterprise-config.md) -- [Network and HTTP configuration](docs/netconfig.md) -- [Credential stores](docs/credstores.md) -- [Architectural overview](docs/architecture.md) -- [Host provider specification](docs/hostprovider.md) -- [Azure Repos OAuth tokens](docs/azrepos-users-and-tokens.md) -- [GitLab support](docs/gitlab.md) +See the [documentation index][docs-index] for links to additional resources. ## Experimental Features -- [Windows broker (experimental)](docs/windows-broker.md) +- [Windows broker (experimental)][gcm-windows-broker] ## Contributing This project welcomes contributions and suggestions. -See the [contributing guide](CONTRIBUTING.md) to get started. +See the [contributing guide][gcm-contributing] to get started. -This project follows [GitHub's Open Source Code of Conduct](CODE_OF_CONDUCT.md). +This project follows [GitHub's Open Source Code of Conduct][gcm-coc]. ## License -We're [MIT](LICENSE) licensed. -When using GitHub logos, please be sure to follow the [GitHub logo guidelines](https://github.com/logos). +We're [MIT][gcm-license] licensed. +When using GitHub logos, please be sure to follow the +[GitHub logo guidelines][github-logos]. + +[azure-devops]: https://dev.azure.com/ +[azure-devops-ssh]: https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops +[bitbucket]: https://bitbucket.org +[bitbucket-ssh]: https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html +[build-status-badge]: https://github.com/GitCredentialManager/git-credential-manager/actions/workflows/continuous-integration.yml/badge.svg +[docs-index]: docs/README.md +[dotnet]: https://dotnet.microsoft.com +[git-credential-helper]: https://git-scm.com/docs/gitcredentials +[gcm]: https://github.com/GitCredentialManager/git-credential-manager +[gcm-coc]: CODE_OF_CONDUCT.md +[gcm-commit-12294990]: https://github.com/git/git/commit/12294990c90e043862be9eb7eb22c3784b526340 +[gcm-contributing]: CONTRIBUTING.md +[gcm-credstores]: docs/credstores.md +[gcm-for-mac-and-linux]: https://github.com/microsoft/Git-Credential-Manager-for-Mac-and-Linux +[gcm-for-windows]: https://github.com/microsoft/Git-Credential-Manager-for-Windows +[gcm-http-proxy]: docs/netconfig.md#http-proxy +[gcm-license]: LICENSE +[gcm-usage]: docs/usage.md +[gcm-windows-broker]: docs/windows-broker.md +[git-tools-credential-storage]: https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage +[github]: https://github.com +[github-ssh]: https://help.github.com/en/articles/connecting-to-github-with-ssh +[github-logos]: https://github.com/logos +[install]: docs/install.md +[ms-package-repos]: https://packages.microsoft.com/repos/ +[workflow-status]: https://github.com/GitCredentialManager/git-credential-manager/actions/workflows/continuous-integration.yml diff --git a/SECURITY.md b/SECURITY.md index 8785fd5ba..994a657e2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,8 @@ # Security -If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github) +If you discover a security issue in this repo, please submit it through the +[GitHub Security Bug Bounty][hackerone-github] Thanks for helping make GitHub products safe for everyone. + +[hackerone-github]: https://hackerone.com/github diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..3076e1935 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# User documentation + +The following are links to GCM user support documentation: + +- [Frequently asked questions][gcm-faq] +- [Command-line usage][gcm-usage] +- [Configuration options][gcm-config] +- [Environment variables][gcm-env] +- [Enterprise configuration][gcm-enterprise-config] +- [Network and HTTP configuration][gcm-net-config] +- [Credential stores][gcm-credstores] +- [Host provider specification][gcm-host-provider] +- [Azure Repos OAuth tokens][gcm-azure-tokens] +- [GitLab support][gcm-gitlab] + +[gcm-azure-tokens]: azrepos-users-and-tokens.md +[gcm-config]: configuration.md +[gcm-credstores]: credstores.md +[gcm-dev]: development.md +[gcm-enterprise-config]: enterprise-config.md +[gcm-env]: environment.md +[gcm-faq]: faq.md +[gcm-gitlab]: gitlab.md +[gcm-host-provider]: hostprovider.md +[gcm-net-config]: netconfig.md +[gcm-usage]: usage.md \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index 7790dfc8c..2b2da52c0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -44,7 +44,7 @@ library (C#). The library targets .NET Standard as well as .NET Framework. > **Note** > > The reason for also targeting .NET Framework directly is that the -> `Microsoft.Identity.Client` ([MSAL.NET](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet)) +> `Microsoft.Identity.Client` ([MSAL.NET][msal]) > library requires a .NET Framework target to be able to show the embedded web > browser auth pop-up on Windows platforms. > @@ -52,12 +52,11 @@ library (C#). The library targets .NET Standard as well as .NET Framework. > our own browser pop-up handling code on .NET meaning both Windows and > Mac. We haven't yet gotten around to exploring this. > -> See [this](https://github.com/GitCredentialManager/git-credential-manager/issues/113) -> issue for more information. +> See [GCM issue 113][issue-113] for more information. The entry-point for GCM can be found in the `Git-Credential-Manager` project, a console application that targets both .NET and .NET Framework. -This project emits the `git-credential-manager-core(.exe)` executable, and +This project emits the `git-credential-manager(.exe)` executable, and contains very little code - registration of all supported host providers and running the `Application` object found in `Core`. @@ -80,9 +79,9 @@ helpers on Windows. ### Cross-platform UI -We hope to be able to migrate the WPF/Windows only helpers to [Avalonia](https://avaloniaui.net/) -in order to gain cross-platform graphical user interface support. See [this](https://github.com/GitCredentialManager/git-credential-manager/issues/136) -issue for up-to-date progress on this effort. +We hope to be able to migrate the WPF/Windows only helpers to [Avalonia][avalonia] +in order to gain cross-platform graphical user interface support. See +[GCM issue 136][issue-136] for up-to-date progress on this effort. ### Microsoft authentication @@ -148,7 +147,7 @@ Git Credential Manager maintains a set of known commands including GCM also maintains a set of known, registered host providers that implement the `IHostProvider` interface. Providers register themselves by adding an instance of the provider to the `Application` object via the `RegisterProvider` -method [in `Core.Program`](../src/shared/Git-Credential-Manager/Program.cs). +method in [`Core.Program`][core-program]. The `GenericHostProvider` is registered last so that it can handle all other HTTP-based remotes as a catch-all, and provide basic username/password auth and detect the presence of Windows Integrated Authentication (Kerberos, NTLM, @@ -162,7 +161,7 @@ The `Get|Store|EraseCommand`s consult the host provider registry for the most appropriate host provider. The default registry implementation select the a host provider by asking each registered provider in turn if they understand the request. The provider selection can be overridden by the user via the -[`credential.provider`](configuration.md#credentialprovider) or [`GCM_PROVIDER`](environment.md#GCM_PROVIDER) +[`credential.provider`][credential-provider] or [`GCM_PROVIDER`][gcm-provider] configuration and environment variable respectively (3). The `Get|Store|EraseCommand`s call the corresponding @@ -277,3 +276,11 @@ operation/authentication. The `ITrace` component can be found on the `ICommandContext` object or passed in directly to some constructors. Verbose and diagnostic information is be written to the trace object in most places of GCM. + +[avalonia]: https://avaloniaui.net/ +[core-program]: ../src/shared/Git-Credential-Manager/Program.cs +[credential-provider]: configuration.md#credentialprovider +[issue-113]: https://github.com/GitCredentialManager/git-credential-manager/issues/113 +[issue-136]: https://github.com/GitCredentialManager/git-credential-manager/issues/136 +[gcm-provider]: environment.md#GCM_PROVIDER +[msal]: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet diff --git a/docs/autodetect.md b/docs/autodetect.md index 398b7598f..5f1390f72 100644 --- a/docs/autodetect.md +++ b/docs/autodetect.md @@ -18,8 +18,8 @@ In order to detect which host provider to use for a self-hosted instance, each provider can provide some heuristic matching of the hostname. For example any hostname that begins "github.*" will be matched to the GitHub host provider. -If a heuristic matches incorrectly, you can always [explicitly configure](#manual-configuration) -GCM to use a particular provider. +If a heuristic matches incorrectly, you can always +[explicitly configure][explicit-config] GCM to use a particular provider. ## Remote URL probing @@ -28,19 +28,20 @@ URL and inspect HTTP response headers to try and detect a self-hosted instance. This network call is only performed if neither an exact nor fuzzy match by hostname can be made. Only one HTTP `HEAD` call is made per credential request -received by Git. To avoid this network call, please [explicitly configure](#explicit-configuration) -the host provider for your self-hosted instance. +received by Git. To avoid this network call, please +[explicitly configure][explicit-config] the host provider for your self-hosted +instance. After a successful detection of the host provider, GCM will automatically set -the [`credential.provider` configuration entry](configuration.md#credentialprovider) +the [`credential.provider`][credential-provider] configuration entry for that remote to avoid needing to perform this expensive network call in future requests. ### Timeout You can control how long GCM will wait for a response to the remote network call -by setting the [`GCM_AUTODETECT_TIMEOUT`](environment.md#GCM_AUTODETECT_TIMEOUT) -environment variable, or the [`credential.autoDetectTimeout`](configuration.md#credentialautodetecttimeout) +by setting the [`GCM_AUTODETECT_TIMEOUT`][gcm-autodetect-timeout] environment +variable, or the [`credential.autoDetectTimeout`][credential-autoDetectTimeout] Git configuration setting to the maximum number of milliseconds to wait. The default value is 2000 milliseconds (2 seconds). You can prevent the network @@ -52,9 +53,9 @@ If the auto-detection mechanism fails to select the correct host provider, or if the remote probing network call is causing performance issues, you can configure GCM to always use a particular host provider, for a given remote URL. -You can either use the the [`GCM_PROVIDER`](environment.md#GCM_PROVIDER) -environment variable, or the [`credential.provider`](configuration.md#credentialprovider) -Git configuration setting for this purpose. +You can either use the the [`GCM_PROVIDER`][gcm-provider] environment variable, +or the [`credential.provider`][credential-provider] Git configuration setting +for this purpose. For example to tell GCM to always use the GitHub host provider for the "ghe.example.com" hostname, you can run the following command: @@ -62,3 +63,9 @@ For example to tell GCM to always use the GitHub host provider for the ```shell git config --global credential.ghe.example.com.provider github ``` + +[credential-autoDetectTimeout]: configuration.md#credentialautodetecttimeout +[credential-provider]: configuration.md#credentialprovider +[explicit-config]: #manual-configuration +[gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT +[gcm-provider]: environment.md#GCM_PROVIDER diff --git a/docs/azrepos-users-and-tokens.md b/docs/azrepos-users-and-tokens.md index 1aab32262..d5b58186b 100644 --- a/docs/azrepos-users-and-tokens.md +++ b/docs/azrepos-users-and-tokens.md @@ -8,8 +8,8 @@ The Azure Repos host provider supports creating multiple types of credential: - Microsoft identity OAuth tokens To select which type of credential the Azure Repos host provider will create -and use, you can set the [`credential.azreposCredentialType`](configuration.md#credentialazreposcredentialtype) -configuration entry (or [`GCM_AZREPOS_CREDENTIALTYPE`](environment.md#GCM_AZREPOS_CREDENTIALTYPE) +and use, you can set the [`credential.azreposCredentialType`][credential-azreposCredentialType] +configuration entry (or [`GCM_AZREPOS_CREDENTIALTYPE`][gcm-azrepos-credential-type] environment variable). ### Azure DevOps personal access tokens @@ -18,7 +18,7 @@ Historically, the only option supported by the Azure Repos host provider was Azure DevOps Personal Access Tokens (PATs). These PATs are only used by Azure DevOps, and must be [managed through the Azure -DevOps user settings page](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page) or [REST API](https://docs.microsoft.com/en-gb/rest/api/azure/devops/tokens/pats). +DevOps user settings page][azure-devops-pats] or [REST API][azure-devops-api]. PATs have a limited lifetime and new tokens must be created once they expire. In Git Credential Manager, when a PAT expired (or was manually revoked) this @@ -70,7 +70,7 @@ In advanced scenarios (such as using multiple accounts) you can interact with and manage remembered user accounts using the 'azure-repos' provider command: ```shell -git-credential-manager-core azure-repos [ list | bind | unbind | ... ] +git-credential-manager azure-repos [ list | bind | unbind | ... ] ``` ##### Listing remembered accounts @@ -79,7 +79,7 @@ You can list all bound user accounts by Git Credential Manager for each Azure DevOps organization using the `list` command: ```shell -$ git-credential-manager-core azure-repos list +$ git-credential-manager azure-repos list contoso: (global) -> alice@contoso.com fabrikam: @@ -105,7 +105,7 @@ the `.git/config` file. If there are local bindings in a repository you can show them with the `list` command: ```shell -~/myrepo$ git-credential-manager-core azure-repos list +~/myrepo$ git-credential-manager azure-repos list contoso: (global) -> alice@contoso.com (local) -> alice-alt@contoso.com @@ -118,7 +118,7 @@ To create a local binding, use the `bind` command with the `--local` option when inside a repository: ```shell -~/myrepo$ git-credential-manager-core azure-repos bind --local contoso alice-alt@contso.com +~/myrepo$ git-credential-manager azure-repos bind --local contoso alice-alt@contso.com ``` ```diff @@ -132,7 +132,7 @@ inside a repository: To have Git Credential Manager forget a user account, use the `unbind` command: ```shell -git-credential-manager-core azure-repos unbind fabrikam +git-credential-manager azure-repos unbind fabrikam ``` ```diff @@ -151,7 +151,7 @@ To forget or remove a local binding, within the repository run the `unbind` command with the `--local` option: ```shell -~/myrepo$ git-credential-manager-core azure-repos unbind --local contoso +~/myrepo$ git-credential-manager azure-repos unbind --local contoso ``` ```diff @@ -170,7 +170,7 @@ To show which accounts are being used for each Git remote in a repository use the `list` command with the `--show-remotes` option: ```shell -~/myrepo$ git-credential-manager-core azure-repos list --show-remotes +~/myrepo$ git-credential-manager azure-repos list --show-remotes contoso: (global) -> alice@contoso.com origin: @@ -188,8 +188,7 @@ inherited). To associate a user account with a particular Git remote you must manually edit the remote URL using `git config` commands to include the username in the -[user information](https://tools.ietf.org/html/rfc3986#section-3.2.1) part of -the URL. +[user information][rfc3986-s321] part of the URL. ```shell git config --local remote.origin.url https://alice-alt%40contoso.com@contoso.visualstudio.com/project/_git/repo @@ -209,7 +208,7 @@ The `list --show-remotes` command will show the user account specified in the remote URL: ```shell -~/myrepo$ git-credential-manager-core azure-repos list --show-remotes +~/myrepo$ git-credential-manager azure-repos list --show-remotes contoso: (global) -> alice@contoso.com origin: @@ -218,3 +217,9 @@ contoso: fabrikam: (global) -> alice@fabrikam.com ``` + +[azure-devops-pats]: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page +[credential-azreposCredentialType]: configuration.md#credentialazreposcredentialtype +[gcm-azrepos-credential-type]: environment.md#GCM_AZREPOS_CREDENTIALTYPE +[azure-devops-api]: https://docs.microsoft.com/en-gb/rest/api/azure/devops/tokens/pats +[rfc3986-s321]: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1 diff --git a/docs/bitbucket-development.md b/docs/bitbucket-development.md index dc7140770..76605223f 100644 --- a/docs/bitbucket-development.md +++ b/docs/bitbucket-development.md @@ -1,82 +1,202 @@ # Bitbucket Authentication, 2FA and OAuth -By default for authenticating against private Git repositories Bitbucket supports SSH and username/password Basic Auth over HTTPS. -Username/password Basic Auth over HTTPS is also available for REST API access. -Additionally Bitbucket supports App-specific passwords which can be used via Basic Auth as username/app-specific-password. - -To enhance security Bitbucket offers optional Two-Factor Authentication (2FA). When 2FA is enabled username/password Basic Auth access to the REST APIs and to Git repositories is suspended. -At that point users are left with the choice of username/apps-specific-password Basic Auth for REST APIs and Git interactions, OAuth for REST APIs and Git/Hg interactions or SSH for Git/HG interactions and one of the previous choices for REST APIs. -SSH and REST API access are beyond the scope of this document. -Read about [Bitbucket's 2FA implementation](https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html). - -App-specific passwords are not particularly user friendly as once created Bitbucket hides their value, even from the owner. -They are intended for use within application that talk to Bitbucket where application can remember and use the app-specific-password. -[Additional information](https://confluence.atlassian.com/display/BITBUCKET/App+passwords). - -OAuth is the intended authentication method for user interactions with HTTPS remote URL for Git repositories when 2FA is active. -Essentially once a client application has an OAuth access token it can be used in place of a user's password. -Read more about information [Bitbucket's OAuth implementation](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html). - -Bitbucket's OAuth implementation follows the standard specifications for OAuth 2.0, which is out of scope for this document. -However it implements a comparatively rare part of OAuth 2.0 Refresh Tokens. -Bitbucket's Access Token's expire after 1 hour if not revoked, as opposed to GitHub's that expire after 1 year. -When GitHub's Access Tokens expire the user must anticipate in the standard OAuth authentication flow to get a new Access Token. Since this occurs, in theory, once per year this is not too onerous. -Since Bitbucket's Access Tokens expire every hour it is too much to expect a user to go through the OAuth authentication flow every hour. -Bitbucket implements refresh Tokens. -Refresh Tokens are issued to the client application at the same time as Access Tokens. -They can only be used to request a new Access Token, and then only if they have not been revoked. -As such the support for Bitbucket and the use of its OAuth in the Git Credentials Manager differs significantly from how VSTS and GitHub are implemented. -This is explained in more detail below. +By default for authenticating against private Git repositories Bitbucket +supports SSH and username/password Basic Auth over HTTPS. Username/password +Basic Auth over HTTPS is also available for REST API access. Additionally +Bitbucket supports App-specific passwords which can be used via Basic Auth as +username/app-specific-password. + +To enhance security Bitbucket offers optional Two-Factor Authentication (2FA). +When 2FA is enabled username/password Basic Auth access to the REST APIs and to +Git repositories is suspended. At that point users are left with the choice of +username/apps-specific-password Basic Auth for REST APIs and Git interactions, +OAuth for REST APIs and Git/Hg interactions or SSH for Git/HG interactions and +one of the previous choices for REST APIs. SSH and REST API access are beyond +the scope of this document. Read about [Bitbucket's 2FA implementation][2fa-impl]. + +App-specific passwords are not particularly user friendly as once created +Bitbucket hides their value, even from the owner. They are intended for use +within application that talk to Bitbucket where application can remember and use +the app-specific-password. [Additional information][additional-info]. + +OAuth is the intended authentication method for user interactions with HTTPS +remote URL for Git repositories when 2FA is active. Essentially once a client +application has an OAuth access token it can be used in place of a user's +password. Read more about information [Bitbucket's OAuth implementation][oauth-impl]. + +Bitbucket's OAuth implementation follows the standard specifications for OAuth +2.0, which is out of scope for this document. However it implements a +comparatively rare part of OAuth 2.0 Refresh Tokens. Bitbucket's Access Token's +expire after 1 hour if not revoked, as opposed to GitHub's that expire after 1 +year. When GitHub's Access Tokens expire the user must anticipate in the +standard OAuth authentication flow to get a new Access Token. Since this occurs, +in theory, once per year this is not too onerous. Since Bitbucket's Access +Tokens expire every hour it is too much to expect a user to go through the OAuth +authentication flow every hour.Bitbucket implements refresh Tokens. +Refresh Tokens are issued to the client application at the same time as Access +Tokens. They can only be used to request a new Access Token, and then only if +they have not been revoked. As such the support for Bitbucket and the use of its +OAuth in the Git Credentials Manager differs significantly from how VSTS and +GitHub are implemented. This is explained in more detail below. ## Multiple User Accounts -Unlike the GitHub implementation within the Git Credential Manager, the Bitbucket implementation stores 'secrets', passwords, app-specific passwords, or OAuth tokens, with usernames in the [Windows Credential Manager](https://msdn.microsoft.com/en-us/library/windows/desktop/aa374792(v=vs.85).aspx) vault. +Unlike the GitHub implementation within the Git Credential Manager, the +Bitbucket implementation stores 'secrets', passwords, app-specific passwords, or +OAuth tokens, with usernames in the [Windows Credential Manager][wincred-manager] +vault. -Depending on the circumstances this means either saving an explicit username in to the Windows Credential Manager/Vault or including the username in the URL used as the identifying key of entries in the Windows Credential Manager vault, i.e. using a key such as `git:https://mminns@bitbucket.org/` rather than `git:https://bitbucket.org`. -This means that the Bitbucket implementation in the GCM can support multiple accounts, and usernames, for a single user against Bitbucket, e.g. a personal account and a work account. +Depending on the circumstances this means either saving an explicit username in +to the Windows Credential Manager/Vault or including the username in the URL +used as the identifying key of entries in the Windows Credential Manager vault, +i.e. using a key such as `git:https://mminns@bitbucket.org/` rather than +`git:https://bitbucket.org`. This means that the Bitbucket implementation in the +GCM can support multiple accounts, and usernames, for a single user against +Bitbucket, e.g. a personal account and a work account. ## Authentication User Experience -When the GCM is triggered by Git, the GCM will check the `host` parameter passed to it. -If it contains `bitbucket.org` it will trigger the Bitbucket related processes. +When the GCM is triggered by Git, the GCM will check the `host` parameter passed +to it. If it contains `bitbucket.org` it will trigger the Bitbucket related +processes. ### Basic Authentication -If the GCM needs to prompt the user for credentials they will always be shown an initial dialog where they can enter a username and password. If the `username` parameter was passed into the GCM it is used to pre-populate the username field, although it can be overridden. -When username and password credentials are submitted the GCM will use them to attempt to retrieve a token, for Basic Authentication this token is in effect the password the user just entered. -The GCM retrieves this `token` by checking the password can be used to successfully retrieve the User profile via the Bitbucket REST API. - -If the username and password credentials sent as Basic Authentication credentials works, then the password is identified as the token. The credentials, the username and the password/token, are then stored and the values returned to Git. - -If the request for the User profile via the REST API fails with a 401 return code it indicates the username/password combination is invalid, nothing is stored and nothing is returned to Git. - -However if the request fails with a 403 (Forbidden) return code, this indicates that the username and password are valid but 2FA is enabled on the Bitbucket Account. -When this occurs the user it prompted to complete the OAuth authentication process. +If the GCM needs to prompt the user for credentials they will always be shown an +initial dialog where they can enter a username and password. If the `username` +parameter was passed into the GCM it is used to pre-populate the username field, +although it can be overridden. When username and password credentials are +submitted the GCM will use them to attempt to retrieve a token, for Basic +Authentication this token is in effect the password the user just entered. The +GCM retrieves this `token` by checking the password can be used to successfully +retrieve the User profile via the Bitbucket REST API. + +If the username and password credentials sent as Basic Authentication +credentials works, then the password is identified as the token. The +credentials, the username and the password/token, are then stored and the values +returned to Git. + +If the request for the User profile via the REST API fails with a 401 return +code it indicates the username/password combination is invalid, nothing is +stored and nothing is returned to Git. + +However if the request fails with a 403 (Forbidden) return code, this indicates +that the username and password are valid but 2FA is enabled on the Bitbucket +Account. When this occurs the user it prompted to complete the OAuth +authentication process. ### OAuth -OAuth authentication prompts the User with a new dialog where they can trigger OAuth authentication. -This involves opening a browser request to `_https://bitbucket.org/site/oauth2/authorize?response_type=code&client_id={consumerkey}&state=authenticated&scope={scopes}&redirect_uri=http://localhost:34106/_`. -This will trigger a flow on Bitbucket where the user must login, potentially including a 2FA prompt, and authorize the GCM to access Bitbucket with the specified scopes. -The GCM will spawn a temporary, local webserver, listening on port 34106, to handle the OAuth redirect/callback. -Assuming the user successfully logins into Bitbucket and authorizes the GCM this callback will include the Access and Refresh Tokens. +OAuth authentication prompts the User with a new dialog where they can trigger +OAuth authentication. This involves opening a browser request to `_https://bitbucket.org/site/oauth2/authorize?response_type=code&client_id={consumerkey}&state=authenticated&scope={scopes}&redirect_uri=http://localhost:34106/_`. +This will trigger a flow on Bitbucket where the user must login, potentially +including a 2FA prompt, and authorize the GCM to access Bitbucket with the +specified scopes. The GCM will spawn a temporary, local webserver, listening on +port 34106, to handle the OAuth redirect/callback. Assuming the user +successfully logins into Bitbucket and authorizes the GCM this callback will +include the Access and Refresh Tokens. -The Access and Refresh Tokens will be stored against the username and the username/Access Token credentials returned to Git. +The Access and Refresh Tokens will be stored against the username and the +username/Access Token credentials returned to Git. ## On-Premise Bitbucket -On-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC, has a number of differences compared to the cloud instance of Bitbucket, [bitbucket.org](https://bitbucket.org). +On-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC, +has a number of differences compared to the cloud instance of Bitbucket, +[bitbucket.org][bitbucket]. -As far as GCMC is concerned the main difference it doesn't support OAuth so only Basic Authentication is available. +As far as GCMC is concerned the main difference it doesn't support OAuth so only +Basic Authentication is available. -It is possible to test with Bitbucket Server by running it locally using the following command from the Atlassian SDK: +It is possible to test with Bitbucket Server by running it locally using the +following command from the Atlassian SDK: ❯ atlas-run-standalone --product bitbucket -See the developer documentation for [atlas-run-standalone](https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/). +See the developer documentation for [atlas-run-standalone][atlas-run-standalone]. -This will download and run a standalone instance of Bitbucket Server which can be accessed using the credentials `admin`/`admin` at +This will download and run a standalone instance of Bitbucket Server which can +be accessed using the credentials `admin`/`admin` at https://localhost:7990/bitbucket -Atlassian has [documentation](https://developer.atlassian.com/server/framework/atlassian-sdk/) on how to download and install their SDK. +Atlassian has [documentation][atlassian-sdk] on how to download and install +their SDK. + +## OAuth2 Configuration + +Bitbucket DC [7.20](https://confluence.atlassian.com/bitbucketserver/bitbucket-data-center-and-server-7-20-release-notes-1101934428.html) +added support for OAuth2 Incoming Application Links and this can be used to +support OAuth2 authentication for Git. This is especially useful in environments +where Bitbucket uses SSO as it removes the requirement for users to manage +[SSH keys](https://confluence.atlassian.com/display/BITBUCKETSERVER0717/Using+SSH+keys+to+secure+Git+operations) +or manual [HTTP access tokens](https://confluence.atlassian.com/display/BITBUCKETSERVER0717/Personal+access+tokens). + +### Host Configuration + +For more details see +[Bitbucket's documentation on Data Center and Server Application Links to other Applications](https://confluence.atlassian.com/bitbucketserver/link-to-other-applications-1018764620.html) + +Create Incoming OAuth 2 Application Link: + +1. Navigate to Administration/Application Links +1. Create Link + 1. Screen 1 + - External Application [check] + - Incoming Application [check] + 1. Screen 2 + - Name : GCM + - Redirect URL : `http://localhost:34106/` + - Application Permissions : Repositories.Read [check], Repositories.Write [check] + 1. Save + + 1. Copy the `ClientId` and `ClientSecret` to configure GCM + +### Client Configuration + +Set the OAuth2 configuration use the `ClientId` and `ClientSecret` copied above, +(for details see [credential.bitbucketDataCenterOAuthClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId) +and [credential.bitbucketDataCenterOAuthClientSecret](configuration.md#credential.bitbucketDataCenterOAuthClientSecret)) + + ❯ git config --global credential.bitbucketDataCenterOAuthClientId {`Copied ClientId`} + + ❯ git config --global credential.bitbucketDataCenterOAuthClientSecret {`Copied ClientSecret`} + +As described in [Configuration options](configuration.md#Configuration%20options) +the settings can be made more specific to apply only to a specific Bitbucket DC +host by specifying the host url, e.g. https://bitbucket.example.com/ + + + ❯ git config --global credential.https://bitbucket.example.com.bitbucketDataCenterOAuthClientId {`Copied ClientId`} + + ❯ git config --global credential.https://bitbucket.example.com.bitbucketDataCenterOAuthClientSecret {`Copied ClientSecret`} + +Due to the way GCM resolves hosts and determines REST API urls, if the Bitbucket +DC instance is hosted under a relative url (e.g. https://example.com/bitbucket) +it is necessary to configure Git to send the full path to GCM. This is done +using the [credential.useHttpPath](configuration.md#credential.useHttpPath) +setting. + ❯ git config --global credential.https://example.com/bitbucket.usehttppath true + + +If a port number is used in the url of the Bitbucket DC instance the Git +configuration needs to reflect this. However, due to [Issue 608](https://github.com/GitCredentialManager/git-credential-manager/issues/608) +the port is ignored when resolving [credential.bitbucketDataCenterOAuthClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId) +and [credential.bitbucketDataCenterOAuthClientSecret](configuration.md#credential.bitbucketDataCenterOAuthClientSecret). + +For example, a Bitbucket DC host at https://example.com:7990/bitbucket would +require configuration in the form: + + ❯ git config --global credential.https://example.com/bitbucket.bitbucketDataCenterOAuthClientId {`Copied ClientId`} + + ❯ git config --global credential.https://example.com/bitbucket.bitbucketDataCenterOAuthClientSecret {`Copied ClientSecret`} + + ❯ git config --global credential.https://example.com:7990/bitbucket.usehttppath true + +[additional-info]:https://confluence.atlassian.com/display/BITBUCKET/App+passwords +[atlas-run-standalone]: https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/ +[bitbucket]: https://bitbucket.org +[2fa-impl]: https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html +[oauth-impl]: https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html +[atlassian-sdk]: https://developer.atlassian.com/server/framework/atlassian-sdk/ +[wincred-manager]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa374792(v=vs.85).aspx diff --git a/docs/configuration.md b/docs/configuration.md index e295c68d7..03272592a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,23 +1,41 @@ # Configuration options -[Git Credential Manager](usage.md) works out of the box for most users. +[Git Credential Manager][usage] works out of the box for most users. -Git Credential Manager (GCM) can be configured using Git's configuration files, and follows all of the same rules Git does when consuming the files. +Git Credential Manager (GCM) can be configured using Git's configuration files, +and follows all of the same rules Git does when consuming the files. -Global configuration settings override system configuration settings, and local configuration settings override global settings; and because the configuration details exist within Git's configuration files you can use Git's `git config` utility to set, unset, and alter the setting values. All of GCM's configuration settings begin with the term `credential`. +Global configuration settings override system configuration settings, and local +configuration settings override global settings; and because the configuration +details exist within Git's configuration files you can use Git's `git config` +utility to set, unset, and alter the setting values. All of GCM's configuration +settings begin with the term `credential`. -GCM honors several levels of settings, in addition to the standard local \> global \> system tiering Git uses. -URL-specific settings or overrides can be applied to any value in the `credential` namespace with the syntax below. +GCM honors several levels of settings, in addition to the standard local +\> global \> system tiering Git uses. URL-specific settings or overrides can be +applied to any value in the `credential` namespace with the syntax below. -Additionally, GCM respects several GCM-specific [environment variables](environment.md) **which take precedence over configuration options**. System administrators may also configure [default values](enterprise-config.md) for many settings used by GCM. +Additionally, GCM respects several GCM-specific [environment variables][envars] +**which take precedence over configuration options**. System administrators may +also configure [default values][enterprise-config] for many settings used by GCM. -GCM will only be used by Git if it is installed and configured. Use `git config --global credential.helper manager-core` to assign GCM as your credential helper. Use `git config credential.helper` to see the current configuration. +GCM will only be used by Git if it is installed and configured. Use +`git config --global credential.helper manager` to assign GCM as your +credential helper. Use `git config credential.helper` to see the current +configuration. **Example:** -> `credential.microsoft.visualstudio.com.namespace` is more specific than `credential.visualstudio.com.namespace`, which is more specific than `credential.namespace`. +> `credential.microsoft.visualstudio.com.namespace` is more specific than +> `credential.visualstudio.com.namespace`, which is more specific than +> `credential.namespace`. -In the examples above, the `credential.namespace` setting would affect any remote repository; the `credential.visualstudio.com.namespace` would affect any remote repository in the domain, and/or any subdomain (including `www.`) of, 'visualstudio.com'; where as the `credential.microsoft.visualstudio.com.namespace` setting would only be applied to remote repositories hosted at 'microsoft.visualstudio.com'. +In the examples above, the `credential.namespace` setting would affect any +remote repository; the `credential.visualstudio.com.namespace` would affect any +remote repository in the domain, and/or any subdomain (including `www.`) of, +'visualstudio.com'; where as the +`credential.microsoft.visualstudio.com.namespace` setting would only be applied +to remote repositories hosted at 'microsoft.visualstudio.com'. For the complete list of settings GCM understands, see the list below. @@ -25,16 +43,20 @@ For the complete list of settings GCM understands, see the list below. ### credential.interactive -Permit or disable GCM from interacting with the user (showing GUI or TTY prompts). If interaction is required but has been disabled, an error is returned. +Permit or disable GCM from interacting with the user (showing GUI or TTY +prompts). If interaction is required but has been disabled, an error is returned. -This can be helpful when using GCM in headless and unattended environments, such as build servers, where it would be preferable to fail than to hang indefinitely waiting for a non-existent user. +This can be helpful when using GCM in headless and unattended environments, such +as build servers, where it would be preferable to fail than to hang indefinitely +waiting for a non-existent user. To disable interactivity set this to `false` or `0`. #### Compatibility -In previous versions of GCM this setting had a different behavior and accepted other values. -The following table summarizes the change in behavior and the mapping of older values such as `never`: +In previous versions of GCM this setting had a different behavior and accepted +other values. The following table summarizes the change in behavior and the +mapping of older values such as `never`: Value(s)|Old meaning|New meaning -|-|- @@ -50,7 +72,7 @@ git config --global credential.interactive false Defaults to enabled. -**Also see: [GCM_INTERACTIVE](environment.md#GCM_INTERACTIVE)** +**Also see: [GCM_INTERACTIVE][gcm-interactive]** --- @@ -60,7 +82,7 @@ Define the host provider to use when authenticating. ID|Provider -|- -`auto` _(default)_|_\[automatic\]_ ([learn more](autodetect.md)) +`auto` _(default)_|_\[automatic\]_ ([learn more][autodetect]) `azure-repos`|Azure Repos `github`|GitHub `bitbucket`|Bitbucket @@ -69,7 +91,9 @@ ID|Provider Automatic provider selection is based on the remote URL. -This setting is typically used with a scoped URL to map a particular set of remote URLs to providers, for example to mark a host as a GitHub Enterprise instance. +This setting is typically used with a scoped URL to map a particular set of +remote URLs to providers, for example to mark a host as a GitHub Enterprise +instance. #### Example @@ -77,17 +101,19 @@ This setting is typically used with a scoped URL to map a particular set of remo git config --global credential.ghe.contoso.com.provider github ``` -**Also see: [GCM_PROVIDER](environment.md#GCM_PROVIDER)** +**Also see: [GCM_PROVIDER][gcm-provider]** --- ### credential.authority _(deprecated)_ -> This setting is deprecated and should be replaced by `credential.provider` with the corresponding provider ID value. +> This setting is deprecated and should be replaced by `credential.provider` +> with the corresponding provider ID value. > -> Click [here](https://aka.ms/gcm/authority) for more information. +> See the [migration guide][provider-migrate] for more information. -Select the host provider to use when authenticating by which authority is supported by the providers. +Select the host provider to use when authenticating by which authority is +supported by the providers. Authority|Provider(s) -|- @@ -104,7 +130,7 @@ Authority|Provider(s) git config --global credential.ghe.contoso.com.authority github ``` -**Also see: [GCM_AUTHORITY](environment.md#GCM_AUTHORITY-deprecated)** +**Also see: [GCM_AUTHORITY][gcm-authority]** --- @@ -113,7 +139,7 @@ git config --global credential.ghe.contoso.com.authority github Permit or disable GCM from presenting GUI prompts. If an equivalent terminal/ text-based prompt is available, that will be shown instead. -To disable all interactivity see [credential.interactive](#credentialinteractive). +To disable all interactivity see [credential.interactive][credential-interactive]. #### Example @@ -123,7 +149,7 @@ git config --global credential.guiPrompt false Defaults to enabled. -**Also see: [GCM_GUI_PROMPT](environment.md#GCM_GUI_PROMPT)** +**Also see: [GCM_GUI_PROMPT][gcm-gui-prompt]** --- @@ -132,7 +158,7 @@ Defaults to enabled. Set the maximum length of time, in milliseconds, that GCM should wait for a network response during host provider auto-detection probing. -See [here](autodetect.md) for more information. +See [auto-detection][auto-detection] for more information. **Note:** Use a negative or zero value to disable probing altogether. @@ -144,13 +170,15 @@ Defaults to 2000 milliseconds (2 seconds). git config --global credential.autoDetectTimeout -1 ``` -**Also see: [GCM_AUTODETECT_TIMEOUT](environment.md#GCM_AUTODETECT_TIMEOUT)** +**Also see: [GCM_AUTODETECT_TIMEOUT][gcm-autodetect-timeout]** --- ### credential.allowWindowsAuth -Allow detection of Windows Integrated Authentication (WIA) support for generic host providers. Setting this value to `false` will prevent the use of WIA and force a basic authentication prompt when using the Generic host provider. +Allow detection of Windows Integrated Authentication (WIA) support for generic +host providers. Setting this value to `false` will prevent the use of WIA and +force a basic authentication prompt when using the Generic host provider. **Note:** WIA is only supported on Windows. @@ -167,15 +195,16 @@ Value|WIA detection git config --global credential.tfsonprem123.allowWindowsAuth false ``` -**Also see: [GCM_ALLOW_WINDOWSAUTH](environment.md#GCM_ALLOW_WINDOWSAUTH)** +**Also see: [GCM_ALLOW_WINDOWSAUTH][gcm-allow-windowsauth]** --- ### credential.httpProxy _(deprecated)_ -> This setting is deprecated and should be replaced by the [standard `http.proxy` Git configuration option](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy). +> This setting is deprecated and should be replaced by the +> [standard `http.proxy` Git configuration option][git-config-http-proxy]. > -> Click [here](https://aka.ms/gcm/httpproxy) for more information. +> See [HTTP Proxy][http-proxy] for more information. Configure GCM to use the a proxy for network operations. @@ -187,16 +216,18 @@ Configure GCM to use the a proxy for network operations. git config --global credential.httpsProxy http://john.doe:password@proxy.contoso.com ``` -**Also see: [GCM_HTTP_PROXY](environment.md#GCM_HTTP_PROXY-deprecated)** +**Also see: [GCM_HTTP_PROXY][gcm-http-proxy]** --- ### credential.bitbucketAuthModes -Override the available authentication modes presented during Bitbucket authentication. -If this option is not set, then the available authentication modes will be automatically detected. +Override the available authentication modes presented during Bitbucket +authentication. If this option is not set, then the available authentication +modes will be automatically detected. -**Note:** This setting only applies to Bitbucket.org, and not Server or DC instances. +**Note:** This setting only applies to Bitbucket.org, and not Server or DC +instances. **Note:** This setting supports multiple values separated by commas. @@ -212,19 +243,26 @@ _(unset)_|Automatically detect modes git config --global credential.bitbucketAuthModes "oauth,basic" ``` -**Also see: [GCM_BITBUCKET_AUTHMODES](environment.md#GCM_BITBUCKET_AUTHMODES)** +**Also see: [GCM_BITBUCKET_AUTHMODES][gcm-bitbucket-authmodes]** --- ### credential.bitbucketAlwaysRefreshCredentials -Forces GCM to ignore any existing stored Basic Auth or OAuth access tokens and always run through the process to refresh the credentials before returning them to Git. +Forces GCM to ignore any existing stored Basic Auth or OAuth access tokens and +always run through the process to refresh the credentials before returning them +to Git. -This is especially relevant to OAuth credentials. Bitbucket.org access tokens expire after 2 hours, after that the refresh token must be used to get a new access token. +This is especially relevant to OAuth credentials. Bitbucket.org access tokens +expire after 2 hours, after that the refresh token must be used to get a new +access token. -Enabling this option will improve performance when using Oauth2 and interacting with Bitbucket.org if, on average, commits are done less frequently than every 2 hours. +Enabling this option will improve performance when using Oauth2 and interacting +with Bitbucket.org if, on average, commits are done less frequently than every +2 hours. -Enabling this option will decrease performance when using Basic Auth by requiring the user the re-enter credentials everytime. +Enabling this option will decrease performance when using Basic Auth by +requiring the user the re-enter credentials every time. Value|Refresh Credentials Before Returning -|- @@ -234,19 +272,93 @@ Value|Refresh Credentials Before Returning #### Example ```shell -git config --global credential.bitbucketAlwaysRefreshCredentials 1 +git config --global credential.bitbucketAlwaysRefreshCredentials true ``` Defaults to false/disabled. -**Also see: [GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS](environment.md#GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS)** +**Also see: [GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS][gcm-bitbucket-always-refresh-credentials]** + +--- + +### credential.bitbucketValidateStoredCredentials + +Forces GCM to validate any stored credentials before returning them to Git. It +does this by calling a REST API resource that requires authentication. + +Disabling this option reduces the HTTP traffic within GCM when it is retrieving +credentials. This may improve user performance, but will increase the number of +times Git remote calls fail to authenticate with the host and therefore require +the user to re-try the Git remote call. + +Enabling this option helps ensure Git is always provided with valid credentials. + +Value|Validate credentials +-|- +`true`, `1`, `yes`, `on`_(default)_|Always +`false`, `0`, `no`, `off`|Never + +#### Example + +```shell +git config --global credential.bitbucketValidateStoredCredentials true +``` + +Defaults to true/enabled. + +**Also see: [GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS](environment.md#GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS)** + +--- + +### credential.bitbucketDataCenterOAuthClientId + +To use OAuth with Bitbucket DC it is necessary to create an external, incoming +[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html). + +It is then necessary to configure the local GCM installation with both the OAuth +[ClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId) and +[ClientSecret](configuration.md#credential.bitbucketDataCenterOauthSecret) from +the AppLink. + +#### Example + +```shell +git config --global credential.bitbucketDataCenterOAuthClientId 1111111111111111111 +``` + +Defaults to undefined. + +**Also see: [GCM_BITBUCKET_DATACENTER_CLIENTID](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTID)** + +--- + +### credential.bitbucketDataCenterOAuthClientSecret + +To use OAuth with Bitbucket DC it is necessary to create an external, incoming +[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html). + +It is then necessary to configure the local GCM installation with both the OAuth +[ClientId](configuration.md#credential.bitbucketDataCenterOAuthClientId) and +[ClientSecret](configuration.md#credential.bitbucketDataCenterOauthSecret) +from the AppLink. + +#### Example + +```shell +git config --global credential.bitbucketDataCenterOAuthClientSecret 222222222222222222222 +``` + +Defaults to undefined. + +**Also see: [GCM_BITBUCKET_DATACENTER_CLIENTSECRET](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTSECRET)** --- ### credential.gitHubAuthModes -Override the available authentication modes presented during GitHub authentication. -If this option is not set, then the available authentication modes will be automatically detected. +Override the available authentication modes presented during GitHub +authentication. If this option is not set, then the available authentication +modes will be automatically detected. **Note:** This setting supports multiple values separated by commas. @@ -265,14 +377,15 @@ _(unset)_|Automatically detect modes git config --global credential.gitHubAuthModes "oauth,basic" ``` -**Also see: [GCM_GITHUB_AUTHMODES](environment.md#GCM_GITHUB_AUTHMODES)** +**Also see: [GCM_GITHUB_AUTHMODES][gcm-github-authmodes]** --- ### credential.gitLabAuthModes -Override the available authentication modes presented during GitLab authentication. -If this option is not set, then the available authentication modes will be automatically detected. +Override the available authentication modes presented during GitLab +authentication. If this option is not set, then the available authentication +modes will be automatically detected. **Note:** This setting supports multiple values separated by commas. @@ -289,14 +402,15 @@ _(unset)_|Automatically detect modes git config --global credential.gitLabAuthModes "browser" ``` -**Also see: [GCM_GITLAB_AUTHMODES](environment.md#GCM_GITLAB_AUTHMODES)** +**Also see: [GCM_GITLAB_AUTHMODES][gcm-gitlab-authmodes]** --- ### credential.namespace -Use a custom namespace prefix for credentials read and written in the OS credential store. -Credentials will be stored in the format `{namespace}:{service}`. +Use a custom namespace prefix for credentials read and written in the OS +credential store. Credentials will be stored in the format +`{namespace}:{service}`. Defaults to the value `git`. @@ -306,7 +420,7 @@ Defaults to the value `git`. git config --global credential.namespace "my-namespace" ``` -**Also see: [GCM_NAMESPACE](environment.md#GCM_NAMESPACE)** +**Also see: [GCM_NAMESPACE][gcm-namespace]** --- @@ -314,20 +428,22 @@ git config --global credential.namespace "my-namespace" Select the type of credential store to use on supported platforms. -Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset on Linux. +Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset +on Linux. -**Note:** See more information about configuring secret stores [here](credstores.md). +**Note:** See more information about configuring secret stores in +[cred-stores][cred-stores]. Value|Credential Store|Platforms -|-|- _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|- `wincredman`|Windows Credential Manager (not available over SSH).|Windows -`dpapi`|DPAPI protected files. Customize the DPAPI store location with [credential.dpapiStorePath](#credentialdpapistorepath)|Windows +`dpapi`|DPAPI protected files. Customize the DPAPI store location with [credential.dpapiStorePath][credential-dpapistorepath]|Windows `keychain`|macOS Keychain.|macOS -`secretservice`|[freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/) via [libsecret](https://wiki.gnome.org/Projects/Libsecret) (requires a graphical interface to unlock secret collections).|Linux -`gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility](https://www.passwordstore.org/) (requires GPG and `pass` to initialize the store).|macOS, Linux -`cache`|Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache).|Windows, macOS, Linux -`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`](#credentialplaintextstorepath).|Windows, macOS, Linux +`secretservice`|[freedesktop.org Secret Service API][freedesktop-ss] via [libsecret][libsecret] (requires a graphical interface to unlock secret collections).|Linux +`gpg`|Use GPG to store encrypted files that are compatible with the [pass][pass] (requires GPG and `pass` to initialize the store).|macOS, Linux +`cache`|Git's built-in [credential cache][credential-cache].|Windows, macOS, Linux +`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`][credential-plaintextstorepath].|Windows, macOS, Linux #### Example @@ -335,15 +451,14 @@ _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|- git config --global credential.credentialStore gpg ``` -**Also see: [GCM_CREDENTIAL_STORE](environment.md#GCM_CREDENTIAL_STORE)** +**Also see: [GCM_CREDENTIAL_STORE][gcm-credential-store]** --- ### credential.cacheOptions -Pass [options](https://git-scm.com/docs/git-credential-cache#_options) -to the Git credential cache when -[`credential.credentialStore`](#credentialcredentialstore) +Pass [options][cache-options] to the Git credential cache when +[`credential.credentialStore`][credential-credentialstore] is set to `cache`. This allows you to select a different amount of time to cache credentials (the default is 900 seconds) by passing `"--timeout "`. Use of other options like `--socket` is untested @@ -357,13 +472,14 @@ Defaults to empty. git config --global credential.cacheOptions "--timeout 300" ``` -**Also see: [GCM_CREDENTIAL_CACHE_OPTIONS](environment.md#GCM_CREDENTIAL_CACHE_OPTIONS)** +**Also see: [GCM_CREDENTIAL_CACHE_OPTIONS][gcm-credential-cache-options]** --- ### credential.plaintextStorePath -Specify a custom directory to store plaintext credential files in when [`credential.credentialStore`](#credentialcredentialstore) is set to `plaintext`. +Specify a custom directory to store plaintext credential files in when +[`credential.credentialStore`][credential-credentialstore] is set to `plaintext`. Defaults to the value `~/.gcm/store` or `%USERPROFILE%\.gcm\store`. @@ -373,13 +489,14 @@ Defaults to the value `~/.gcm/store` or `%USERPROFILE%\.gcm\store`. git config --global credential.plaintextStorePath /mnt/external-drive/credentials ``` -**Also see: [GCM_PLAINTEXT_STORE_PATH](environment.md#GCM_PLAINTEXT_STORE_PATH)** +**Also see: [GCM_PLAINTEXT_STORE_PATH][gcm-plaintext-store-path]** --- ### credential.dpapiStorePath -Specify a custom directory to store DPAPI protected credential files in when [`credential.credentialStore`](#credentialcredentialstore) is set to `dpapi`. +Specify a custom directory to store DPAPI protected credential files in when +[`credential.credentialStore`][credential-credentialstore] is set to `dpapi`. Defaults to the value `%USERPROFILE%\.gcm\dpapi_store`. @@ -389,17 +506,18 @@ Defaults to the value `%USERPROFILE%\.gcm\dpapi_store`. git config --global credential.dpapiStorePath D:\credentials ``` -**Also see: [GCM_DPAPI_STORE_PATH](environment.md#GCM_DPAPI_STORE_PATH)** +**Also see: [GCM_DPAPI_STORE_PATH][gcm-dpapi-store-path]** --- ### credential.msauthFlow -Specify which authentication flow should be used when performing Microsoft authentication and an interactive flow is required. +Specify which authentication flow should be used when performing Microsoft +authentication and an interactive flow is required. Defaults to `auto`. -**Note:** If [`credential.msauthUseBroker`](#credentialmsauthusebroker-experimental) is set +**Note:** If [`credential.msauthUseBroker`][credential-msauthusebroker] is set to `true` and the operating system authentication broker is available, all flows will be delegated to the broker. If both of those things are true, then the value of `credential.msauthFlow` has no effect. @@ -417,7 +535,7 @@ Value|Authentication Flow git config --global credential.msauthFlow devicecode ``` -**Also see: [GCM_MSAUTH_FLOW](environment.md#GCM_MSAUTH_FLOW)** +**Also see: [GCM_MSAUTH_FLOW][gcm-msauth-flow]** --- @@ -427,7 +545,9 @@ Use the operating system account manager where available. Defaults to `false`. This default is subject to change in the future. -_**Note:** before you enable this option on Windows, please [review the details](windows-broker.md) about what this means to your local Windows user account._ +_**Note:** before you enable this option on Windows, please review the +[Windows Broker][wam] details for what this means to your local Windows user +account._ Value|Description -|- @@ -440,21 +560,29 @@ Value|Description git config --global credential.msauthUseBroker true ``` -**Also see: [GCM_MSAUTH_USEBROKER](environment.md#GCM_MSAUTH_USEBROKER-experimental)** +**Also see: [GCM_MSAUTH_USEBROKER][gcm-msauth-usebroker]** --- ### credential.useHttpPath -Tells Git to pass the entire repository URL, rather than just the hostname, when calling out to a credential provider. (This setting [comes from Git itself](https://git-scm.com/docs/gitcredentials/#Documentation/gitcredentials.txt-useHttpPath), not GCM.) +Tells Git to pass the entire repository URL, rather than just the hostname, when +calling out to a credential provider. (This setting +[comes from Git itself][use-http-path], not GCM.) Defaults to `false`. -**Note:** GCM sets this value to `true` for `dev.azure.com` (Azure Repos) hosts after installation by default. +**Note:** GCM sets this value to `true` for `dev.azure.com` (Azure Repos) hosts +after installation by default. -This is because `dev.azure.com` alone is not enough information to determine the correct Azure authentication authority - we require a part of the path. The fallout of this is that for `dev.azure.com` remote URLs we do not support storing credentials against the full-path. We always store against the `dev.azure.com/org-name` stub. +This is because `dev.azure.com` alone is not enough information to determine the +correct Azure authentication authority - we require a part of the path. The +fallout of this is that for `dev.azure.com` remote URLs we do not support +storing credentials against the full-path. We always store against the +`dev.azure.com/org-name` stub. -In order to use Azure Repos and store credentials against a full-path URL, you must use the `org-name.visualstudio.com` remote URL format instead. +In order to use Azure Repos and store credentials against a full-path URL, you +must use the `org-name.visualstudio.com` remote URL format instead. Value|Git Behavior -|- @@ -463,7 +591,9 @@ Value|Git Behavior #### Example -On Windows using GitHub, for a user whose login is `alice`, and with `credential.useHttpPath` set to `false` (or not set), the following remote URLs will use the same credentials: +On Windows using GitHub, for a user whose login is `alice`, and with +`credential.useHttpPath` set to `false` (or not set), the following remote URLs +will use the same credentials: ```text Credential: "git:https://github.com" (user = alice) @@ -480,7 +610,8 @@ Credential: "git:https://bob@github.com" (user = bob) https://bob@github.com/example/myrepo ``` -Under the same user but with `credential.useHttpPath` set to `true`, these credentials would be used: +Under the same user but with `credential.useHttpPath` set to `true`, these +credentials would be used: ```text Credential: "git:https://github.com/foo/bar" (user = alice) @@ -520,7 +651,7 @@ Value|Description `pat` _(default)_|Azure DevOps personal access tokens `oauth`|Microsoft identity OAuth tokens (AAD or MSA tokens) -More information about Azure Access tokens can be found [here](azrepos-users-and-tokens.md). +Here is more information about [Azure Access tokens][azure-tokens]. #### Example @@ -528,4 +659,46 @@ More information about Azure Access tokens can be found [here](azrepos-users-and git config --global credential.azreposCredentialType oauth ``` -**Also see: [GCM_AZREPOS_CREDENTIALTYPE](environment.md#GCM_AZREPOS_CREDENTIALTYPE)** +**Also see: [GCM_AZREPOS_CREDENTIALTYPE][gcm-azrepos-credentialtype]** + +[auto-detection]: autodetect.md +[azure-tokens]: azrepos-users-and-tokens.md +[use-http-path]: https://git-scm.com/docs/gitcredentials/#Documentation/gitcredentials.txt-useHttpPath +[credential-credentialstore]: #credentialcredentialstore +[credential-dpapistorepath]: #credentialdpapistorepath +[credential-interactive]: #credentialinteractive +[credential-msauthusebroker]: #credentialmsauthusebroker-experimental +[credential-plaintextstorepath]: #credentialplaintextstorepath +[credential-cache]: https://git-scm.com/docs/git-credential-cache +[cred-stores]: credstores.md +[enterprise-config]: enterprise-config.md +[envars]: environment.md +[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/ +[gcm-allow-windowsauth]: environment.md#GCM_ALLOW_WINDOWSAUTH +[gcm-authority]: environment.md#GCM_AUTHORITY-deprecated +[gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT +[gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE +[gcm-bitbucket-always-refresh-credentials]: environment.md#GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS +[gcm-bitbucket-authmodes]: environment.md#GCM_BITBUCKET_AUTHMODES +[gcm-credential-cache-options]: environment.md#GCM_CREDENTIAL_CACHE_OPTIONS +[gcm-credential-store]: environment.md#GCM_CREDENTIAL_STORE +[gcm-dpapi-store-path]: environment.md#GCM_DPAPI_STORE_PATH +[gcm-github-authmodes]: environment.md#GCM_GITHUB_AUTHMODES +[gcm-gitlab-authmodes]:environment.md#GCM_GITLAB_AUTHMODES +[gcm-gui-prompt]: environment.md#GCM_GUI_PROMPT +[gcm-http-proxy]: environment.md#GCM_HTTP_PROXY-deprecated +[gcm-interactive]: environment.md#GCM_INTERACTIVE +[gcm-msauth-flow]: environment.md#GCM_MSAUTH_FLOW +[gcm-msauth-usebroker]: environment.md#GCM_MSAUTH_USEBROKER-experimental +[gcm-namespace]: environment.md#GCM_NAMESPACE +[gcm-plaintext-store-path]: environment.md#GCM_PLAINTEXT_STORE_PATH +[gcm-provider]: environment.md#GCM_PROVIDER +[usage]: usage.md +[git-config-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy +[http-proxy]: netconfig.md#http-proxy +[autodetect]: autodetect.md +[libsecret]: https://wiki.gnome.org/Projects/Libsecret +[provider-migrate]: migration.md#gcm_authority +[cache-options]: https://git-scm.com/docs/git-credential-cache#_options +[pass]: https://www.passwordstore.org/ +[wam]: windows-broker.md diff --git a/docs/credstores.md b/docs/credstores.md index bb206e647..157eaf930 100644 --- a/docs/credstores.md +++ b/docs/credstores.md @@ -5,9 +5,9 @@ There are several options for storing credentials that GCM supports: - Windows Credential Manager - DPAPI protected files - macOS Keychain -- [freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/) -- GPG/[`pass`](https://www.passwordstore.org/) compatible files -- Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache) +- [freedesktop.org Secret Service API][freedesktop-secret-service] +- GPG/[`pass`][passwordstore] compatible files +- Git's built-in [credential cache][credential-cache] - Plaintext files The default credential stores on macOS and Windows are the macOS Keychain and @@ -15,8 +15,8 @@ the Windows Credential Manager, respectively. GCM comes without a default store on Linux distributions. -You can select which credential store to use by setting the [`GCM_CREDENTIAL_STORE`](environment.md#GCM_CREDENTIAL_STORE) -environment variable, or the [`credential.credentialStore`](configuration.md#credentialcredentialstore) +You can select which credential store to use by setting the [`GCM_CREDENTIAL_STORE`][gcm-credential-store] +environment variable, or the [`credential.credentialStore`][credential-store] Git configuration setting. For example: ```shell @@ -49,8 +49,8 @@ This credential store uses the Windows Credential APIs (`wincred.h`) to store data securely in the Windows Credential Manager (also known as the Windows Credential Vault in earlier versions of Windows). -You can [access and manage data in the credential manager](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) -from the control panel, or via the [`cmdkey` command-line tool](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmdkey). +You can [access and manage data in the credential manager][access-windows-credential-manager] +from the control panel, or via the [`cmdkey` command-line tool][cmdkey]. When connecting to a Windows machine over a network session (such as SSH), GCM is unable to persist credentials to the Windows Credential Manager due to @@ -73,7 +73,7 @@ git config --global credential.credentialStore dpapi This credential store uses Windows DPAPI to encrypt credentials which are stored as files in your file system. The file structure is the same as the -[plaintext files credential store](#plaintext-files) except the first line (the +[plaintext files credential store][plaintext-files] except the first line (the secret value) is protected by DPAPI. By default files are stored in `%USERPROFILE%\.gcm\dpapi_store`. This can be @@ -97,10 +97,10 @@ git config --global credential.credentialStore keychain This credential store uses the default macOS Keychain, which is typically the `login` keychain. -You can [manage data stored in the keychain](https://support.apple.com/en-gb/guide/mac-help/mchlf375f392/mac) +You can [manage data stored in the keychain][mac-keychain-management] using the Keychain Access application. -## [freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/) +## [freedesktop.org Secret Service API][freedesktop-secret-service] **Available on:** _Linux_ @@ -119,7 +119,7 @@ tools such as `secret-tool` and `seahorse`. A graphical user interface is required in order to show a secure prompt to request a secret collection be unlocked. -## GPG/[`pass`](https://www.passwordstore.org/) compatible files +## GPG/[`pass`][passwordstore] compatible files **Available on:** _macOS, Linux_ @@ -133,7 +133,7 @@ git config --global credential.credentialStore gpg This credential store uses GPG to encrypt files containing credentials which are stored in your file system. The file structure is compatible with the popular -[`pass`](https://www.passwordstore.org/) tool. By default files are stored in +[`pass`][passwordstore] tool. By default files are stored in `~/.password-store` but this can be configured using the `pass` environment variable `PASSWORD_STORE_DIR`. @@ -176,7 +176,7 @@ export GPG_TTY=$(tty) **Note:** Using `/dev/tty` does not appear to work here - you must use the real TTY device path, as returned by the `tty` utility. -## Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache) +## Git's built-in [credential cache][credential-cache] **Available on:** _macOS, Linux_ @@ -187,17 +187,16 @@ git config --global credential.credentialStore cache ``` This credential store uses Git's built-in ephemeral -[in-memory credential cache](https://git-scm.com/docs/git-credential-cache). +in-memory [credential cache][credential-cache]. This helps you reduce the number of times you have to authenticate but doesn't require storing credentials on persistent storage. It's good for -scenarios like [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/overview) -or [AWS CloudShell](https://aws.amazon.com/cloudshell/), where you don't want to +scenarios like [Azure Cloud Shell][azure-cloudshell] +or [AWS CloudShell][aws-cloudshell], where you don't want to leave credentials on disk but also don't want to re-authenticate on every Git operation. By default, `git credential-cache` stores your credentials for 900 seconds. -That, and any other -[options it accepts](https://git-scm.com/docs/git-credential-cache#_options), +That, and any other [options it accepts][git-credential-cache-options], may be altered by setting them in the environment variable `GCM_CREDENTIAL_CACHE_OPTIONS` or the Git config value `credential.cacheOptions`. (Using the `--socket` option is untested @@ -232,7 +231,8 @@ On POSIX platforms the newly created store directory will have permissions set such that only the owner can `r`ead/`w`rite/e`x`ecute (`700` or `drwx---`). Permissions on existing directories will not be modified. -NB. GCM's plaintext store is distinct from [git-credential-store](https://git-scm.com/docs/git-credential-store), though the formats are similar. The default paths differ. +NB. GCM's plaintext store is distinct from [git-credential-store][git-credential-store], +though the formats are similar. The default paths differ. --- @@ -251,4 +251,16 @@ permissions on this directory such that no other users or applications can access files within. If possible, use a path that exists on an external volume that you take with you and use full-disk encryption. ---- +[access-windows-credential-manager]: https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0 +[aws-cloudshell]: https://aws.amazon.com/cloudshell/ +[azure-cloudshell]: https://docs.microsoft.com/azure/cloud-shell/overview +[cmdkey]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmdkey +[credential-store]: configuration.md#credentialcredentialstore +[credential-cache]: https://git-scm.com/docs/git-credential-cache +[freedesktop-secret-service]: https://specifications.freedesktop.org/secret-service/ +[gcm-credential-store]: environment.md#GCM_CREDENTIAL_STORE +[git-credential-store]: https://git-scm.com/docs/git-credential-store +[mac-keychain-management]: https://support.apple.com/en-gb/guide/mac-help/mchlf375f392/mac +[git-credential-cache-options]: https://git-scm.com/docs/git-credential-cache#_options +[passwordstore]: https://www.passwordstore.org/ +[plaintext-files]: #plaintext-files diff --git a/docs/development.md b/docs/development.md index fdec0d8b7..2b8fcfd41 100644 --- a/docs/development.md +++ b/docs/development.md @@ -6,15 +6,18 @@ Start by cloning this repository: git clone https://github.com/GitCredentialManager/git-credential-manager ``` -You also need the latest version of the .NET SDK which can be downloaded and installed from [here](https://dotnet.microsoft.com/). +You also need the latest version of the .NET SDK which can be downloaded and +installed from the [.NET website][dotnet-web]. ## Building -The `Git-Credential-Manager.sln` solution can be opened and built in Visual Studio, Visual Studio for Mac, Visual Studio Code, or JetBrains Rider. +The `Git-Credential-Manager.sln` solution can be opened and built in Visual +Studio, Visual Studio for Mac, Visual Studio Code, or JetBrains Rider. ### macOS -To build from inside an IDE, make sure to select the `MacDebug` or `MacRelease` solution configurations. +To build from inside an IDE, make sure to select the `MacDebug` or `MacRelease` +solution configurations. To build from the command line, run: @@ -28,7 +31,8 @@ The flat binaries can also be found in `out/osx/Installer.Mac/pkg/Debug/payload` ### Windows -To build from inside an IDE, make sure to select the `WindowsDebug` or `WindowsRelease` solution configurations. +To build from inside an IDE, make sure to select the `WindowsDebug` or +`WindowsRelease` solution configurations. To build from the command line, run: @@ -56,9 +60,12 @@ The flat binaries can also be found in `out/linux/Packaging.Linux/payload/Debug` ## Debugging -To debug from inside an IDE you'll want to set `Git-Credential-Manager` as the startup project, and specify one of `get`, `store`, or `erase` as a program argument. +To debug from inside an IDE you'll want to set `Git-Credential-Manager` as the +startup project, and specify one of `get`, `store`, or `erase` as a program +argument. -To simulate Git interacting with GCM, when you start from your IDE of choice, you'll need to enter the following [information over standard input](https://git-scm.com/docs/git-credential#IOFMT): +To simulate Git interacting with GCM, when you start from your IDE of choice, +you'll need to enter the following [information over standard input][ioformat]: ```text protocol=http @@ -67,7 +74,8 @@ host= ``` -..where `` is a supported hostname such as `github.com`, and `` is a line feed (or CRLF, we support both!). +..where `` is a supported hostname such as `github.com`, and `` is +a line feed (or CRLF, we support both!). You may also include the following optional fields, depending on your scenario: @@ -76,29 +84,36 @@ username= password= ``` -For more information about how Git interacts with credential helpers, please read Git's [documentation](https://git-scm.com/docs/gitcredentials#_custom_helpers). +For more information about how Git interacts with credential helpers, please +read Git's documentation on [custom helpers][custom-helpers]. ### Attaching to a running process -If you want to debug an already running GCM process, set the `GCM_DEBUG` environment variable to `1` or `true`. The process will wait on launch for a debugger to attach before continuing. +If you want to debug an already running GCM process, set the `GCM_DEBUG` +environment variable to `1` or `true`. The process will wait on launch for a +debugger to attach before continuing. -This is useful when debugging interactions between GCM and Git, and you want Git to be the one launching us. +This is useful when debugging interactions between GCM and Git, and you want +Git to be the one launching us. ### Collect trace output -If you want to debug a release build or installation of GCM, you can set the `GCM_TRACE` environment variable to `1` to print trace information to standard error, or to an absolute file path to write trace information to a file. +If you want to debug a release build or installation of GCM, you can set the +`GCM_TRACE` environment variable to `1` to print trace information to standard +error, or to an absolute file path to write trace information to a file. For example: ```shell -$ GCM_TRACE=1 git-credential-manager-core version +$ GCM_TRACE=1 git-credential-manager version > 18:47:56.526712 ...er/Application.cs:69 trace: [RunInternalAsync] Git Credential Manager version 2.0.124-beta+e1ebbe1517 (macOS, .NET 5.0) 'version' > Git Credential Manager version 2.0.124-beta+e1ebbe1517 (macOS, .NET 5.0) ``` ### Code coverage metrics -If you want code coverage metrics these can be generated either from the command line: +If you want code coverage metrics these can be generated either from the command +line: ```shell dotnet test --collect:"XPlat Code Coverage" --settings=./.code-coverage/coverlet.settings.xml @@ -110,7 +125,8 @@ Or via the VSCode Terminal/Run Task: test with coverage ``` -HTML reports can be generated using ReportGenerator, this should be installed during the build process, from the command line: +HTML reports can be generated using ReportGenerator, this should be installed +during the build process, from the command line: ```shell dotnet ~/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage @@ -133,3 +149,24 @@ or ```console report coverage - win ``` + +## Linting Documentation + +Documents are linted using [markdownlint][markdownlint] which can be installed +as a CLI tool via NPM or as an [extension in VSCode][vscode-markdownlint]. See +the [documentation on GitHub][markdownlint]. The configuration used for +markdownlint is in [.markdownlint.jsonc][markdownlint-config]. + +Documents are checked for link validity using [lychee][lychee]. Lychee can be +installed in a variety of ways depending on your platform, see the [docs on GitHub][lychee-docs]. +Some URLs are ignored by lychee, per the [lycheeignore][lycheeignore]. + +[dotnet-web]: https://dotnet.microsoft.com/ +[custom-helpers]: https://git-scm.com/docs/gitcredentials#_custom_helpers +[ioformat]: https://git-scm.com/docs/git-credential#IOFMT +[lychee]: https://lychee.cli.rs/ +[lychee-docs]: https://github.com/lycheeverse/lychee +[lycheeignore]: ../.lycheeignore +[markdownlint]: https://github.com/DavidAnson/markdownlint-cli2 +[markdownlint-config]: ../.markdownlint.jsonc +[vscode-markdownlint]: https://github.com/DavidAnson/vscode-markdownlint diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md index 96ce891d5..bfdc7e302 100644 --- a/docs/enterprise-config.md +++ b/docs/enterprise-config.md @@ -3,8 +3,8 @@ Git Credential Manager (GCM) can be configured using multiple different mechanisms. In order of preference, those mechanisms are: -1. [Environment variables](environment.md) -1. [Standard Git configuration files](configuration.md) +1. [Environment variables][environment] +1. Standard [Git configuration][config] files 1. Repository/local configuration (`.git/config`) 1. User/global configuration (`$HOME/.gitconfig` or `%HOME%\.gitconfig`) 1. Installation/system configuration (`etc/gitconfig`) @@ -50,7 +50,7 @@ By using the Windows Registry, system administrators can use Group Policy to easily set defaults for GCM's settings. The names and possible values of all settings under this key are the same as -those of the [Git configuration](configuration.md) settings. +those of the [Git configuration][config] settings. The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD` (integer). @@ -58,3 +58,6 @@ The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD` ## macOS/Linux Default configuration setting stores has not been implemented. + +[environment]: environment.md +[config]: configuration.md diff --git a/docs/environment.md b/docs/environment.md index 18cfa4774..7c73965a7 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -1,17 +1,22 @@ # Environment variables -[Git Credential Manager](usage.md) works out of the box for most users. Configuration options are available to customize or tweak behavior. +[Git Credential Manager][gcm] works out of the box for most users. Configuration +options are available to customize or tweak behavior. -Git Credential Manager (GCM) can be configured using environment variables. **Environment variables take precedence over [configuration](configuration.md) options and enterprise system administrator [default values](enterprise-config.md)**. +Git Credential Manager (GCM) can be configured using environment variables. +**Environment variables take precedence over [configuration][configuration] +options and enterprise system administrator [default values][default-values]**. -For the complete list of environment variables GCM understands, see the list below. +For the complete list of environment variables GCM understands, see the list +below. ## Available settings ### GCM_TRACE Enables trace logging of all activities. -Configuring Git and GCM to trace to the same location is often desirable, and GCM is compatible and cooperative with `GIT_TRACE`. +Configuring Git and GCM to trace to the same location is often desirable, and +GCM is compatible and cooperative with `GIT_TRACE`. #### Example @@ -29,7 +34,8 @@ export GIT_TRACE=$HOME/git.log export GCM_TRACE=$HOME/git.log ``` -If the value of `GCM_TRACE` is a full path to a file in an existing directory, logs are appended to the file. +If the value of `GCM_TRACE` is a full path to a file in an existing directory, +logs are appended to the file. If the value of `GCM_TRACE` is `true` or `1`, logs are written to standard error. @@ -41,8 +47,8 @@ _No configuration equivalent._ ### GCM_TRACE_SECRETS -Enables tracing of secret and sensitive information, which is by default masked in trace output. -Requires that `GCM_TRACE` is also enabled. +Enables tracing of secret and sensitive information, which is by default masked +in trace output. Requires that `GCM_TRACE` is also enabled. #### Example @@ -60,7 +66,8 @@ export GCM_TRACE=$HOME/gcm.log export GCM_TRACE_SECRETS=1 ``` -If the value of `GCM_TRACE_SECRETS` is `true` or `1`, trace logs will include secret information. +If the value of `GCM_TRACE_SECRETS` is `true` or `1`, trace logs will include +secret information. Defaults to disabled. @@ -70,8 +77,8 @@ _No configuration equivalent._ ### GCM_TRACE_MSAUTH -Enables inclusion of Microsoft Authentication libraries (ADAL, MSAL) logs in GCM trace output. -Requires that `GCM_TRACE` is also enabled. +Enables inclusion of Microsoft Authentication libraries (ADAL, MSAL) logs in GCM +trace output. Requires that `GCM_TRACE` is also enabled. #### Example @@ -89,7 +96,8 @@ export GCM_TRACE=$HOME/gcm.log export GCM_TRACE_MSAUTH=1 ``` -If the value of `GCM_TRACE_MSAUTH` is `true` or `1`, trace logs will include verbose ADAL/MSAL logs. +If the value of `GCM_TRACE_MSAUTH` is `true` or `1`, trace logs will include +verbose ADAL/MSAL logs. Defaults to disabled. @@ -123,16 +131,21 @@ _No configuration equivalent._ ### GCM_INTERACTIVE -Permit or disable GCM from interacting with the user (showing GUI or TTY prompts). If interaction is required but has been disabled, an error is returned. +Permit or disable GCM from interacting with the user (showing GUI or TTY +prompts). If interaction is required but has been disabled, an error is +returned. -This can be helpful when using GCM in headless and unattended environments, such as build servers, where it would be preferable to fail than to hang indefinitely waiting for a non-existent user. +This can be helpful when using GCM in headless and unattended environments, such +as build servers, where it would be preferable to fail than to hang indefinitely +waiting for a non-existent user. To disable interactivity set this to `false` or `0`. #### Compatibility -In previous versions of GCM this setting had a different behavior and accepted other values. -The following table summarizes the change in behavior and the mapping of older values such as `never`: +In previous versions of GCM this setting had a different behavior and accepted +other values. The following table summarizes the change in behavior and the +mapping of older values such as `never`: Value(s)|Old meaning|New meaning -|-|- @@ -156,7 +169,7 @@ export GCM_INTERACTIVE=0 Defaults to enabled. -**Also see: [credential.interactive](configuration.md#credentialinteractive)** +**Also see: [credential.interactive][credential-interactive]** --- @@ -166,7 +179,7 @@ Define the host provider to use when authenticating. ID|Provider -|- -`auto` _(default)_|_\[automatic\]_ ([learn more](autodetect.md)) +`auto` _(default)_|_\[automatic\]_ ([learn more][autodetect]) `azure-repos`|Azure Repos `github`|GitHub `gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_ @@ -174,7 +187,9 @@ ID|Provider Automatic provider selection is based on the remote URL. -This setting is typically used with a scoped URL to map a particular set of remote URLs to providers, for example to mark a host as a GitHub Enterprise instance. +This setting is typically used with a scoped URL to map a particular set of +remote URLs to providers, for example to mark a host as a GitHub Enterprise +instance. #### Example @@ -190,17 +205,19 @@ SET GCM_PROVIDER=github export GCM_PROVIDER=github ``` -**Also see: [credential.provider](configuration.md#credentialprovider)** +**Also see: [credential.provider][credential-provider]** --- ### GCM_AUTHORITY _(deprecated)_ -> This setting is deprecated and should be replaced by `GCM_PROVIDER` with the corresponding provider ID value. +> This setting is deprecated and should be replaced by `GCM_PROVIDER` with the +> corresponding provider ID value. > -> Click [here](https://aka.ms/gcm/authority) for more information. +> See the [migration guide][migration-guide] for more information. -Select the host provider to use when authenticating by which authority is supported by the providers. +Select the host provider to use when authenticating by which authority is +supported by the providers. Authority|Provider(s) -|- @@ -224,7 +241,7 @@ SET GCM_AUTHORITY=github export GCM_AUTHORITY=github ``` -**Also see: [credential.authority](configuration.md#credentialauthority-deprecated)** +**Also see: [credential.authority][credential-authority]** --- @@ -233,7 +250,7 @@ export GCM_AUTHORITY=github Permit or disable GCM from presenting GUI prompts. If an equivalent terminal/ text-based prompt is available, that will be shown instead. -To disable all interactivity see [GCM_INTERACTIVE](#gcm_interactive). +To disable all interactivity see [GCM_INTERACTIVE][gcm-interactive]. #### Example @@ -251,7 +268,7 @@ export GCM_GUI_PROMPT=0 Defaults to enabled. -**Also see: [credential.guiPrompt](configuration.md#credentialguiprompt)** +**Also see: [credential.guiPrompt][credential-guiprompt]** --- @@ -260,7 +277,7 @@ Defaults to enabled. Set the maximum length of time, in milliseconds, that GCM should wait for a network response during host provider auto-detection probing. -See [here](autodetect.md) for more information. +See [autodetection][autodetect] for more information. **Note:** Use a negative or zero value to disable probing altogether. @@ -280,13 +297,15 @@ SET GCM_AUTODETECT_TIMEOUT=-1 export GCM_AUTODETECT_TIMEOUT=-1 ``` -**Also see: [credential.autoDetectTimeout](configuration.md#credentialautodetecttimeout)** +**Also see: [credential.autoDetectTimeout][credential-autodetecttimeout]** --- ### GCM_ALLOW_WINDOWSAUTH -Allow detection of Windows Integrated Authentication (WIA) support for generic host providers. Setting this value to `false` will prevent the use of WIA and force a basic authentication prompt when using the Generic host provider. +Allow detection of Windows Integrated Authentication (WIA) support for generic +host providers. Setting this value to `false` will prevent the use of WIA and +force a basic authentication prompt when using the Generic host provider. **Note:** WIA is only supported on Windows. @@ -311,15 +330,16 @@ SET GCM_ALLOW_WINDOWSAUTH=0 export GCM_ALLOW_WINDOWSAUTH=0 ``` -**Also see: [credential.allowWindowsAuth](environment.md#credentialallowWindowsAuth)** +**Also see: [credential.allowWindowsAuth][credential-allowwindowsauth]** --- ### GCM_HTTP_PROXY _(deprecated)_ -> This setting is deprecated and should be replaced by the [standard `http.proxy` Git configuration option](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy). +> This setting is deprecated and should be replaced by the [standard `http.proxy` +> Git configuration option][git-httpproxy]. > -> Click [here](https://aka.ms/gcm/httpproxy) for more information. +> See the [HTTP proxy configuration][network-http-proxy] for more information. Configure GCM to use the a proxy for network operations. @@ -337,16 +357,18 @@ SET GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com export GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com ``` -**Also see: [credential.httpProxy](configuration.md#credentialhttpProxy-deprecated)** +**Also see: [credential.httpProxy][credential-httpproxy]** --- ### GCM_BITBUCKET_AUTHMODES -Override the available authentication modes presented during Bitbucket authentication. -If this option is not set, then the available authentication modes will be automatically detected. +Override the available authentication modes presented during Bitbucket +authentication. If this option is not set, then the available authentication +modes will be automatically detected. -**Note:** This setting only applies to Bitbucket.org, and not Server or DC instances. +**Note:** This setting only applies to Bitbucket.org, and not Server or DC +instances. **Note:** This setting supports multiple values separated by commas. @@ -368,19 +390,26 @@ SET GCM_BITBUCKET_AUTHMODES="oauth,basic" export GCM_BITBUCKET_AUTHMODES="oauth,basic" ``` -**Also see: [credential.bitbucketAuthModes](configuration.md#credentialbitbucketAuthModes)** +**Also see: [credential.bitbucketAuthModes][credential-bitbucketauthmodes]** --- ### GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS -Forces GCM to ignore any existing stored Basic Auth or OAuth access tokens and always run through the process to refresh the credentials before returning them to Git. +Forces GCM to ignore any existing stored Basic Auth or OAuth access tokens and +always run through the process to refresh the credentials before returning them +to Git. -This is especially relevant to OAuth credentials. Bitbucket.org access tokens expire after 2 hours, after that the refresh token must be used to get a new access token. +This is especially relevant to OAuth credentials. Bitbucket.org access tokens +expire after 2 hours, after that the refresh token must be used to get a new +access token. -Enabling this option will improve performance when using Oauth2 and interacting with Bitbucket.org if, on average, commits are done less frequently than every 2 hours. +Enabling this option will improve performance when using Oauth2 and interacting +with Bitbucket.org if, on average, commits are done less frequently than every 2 +hours. -Enabling this option will decrease performance when using Basic Auth by requiring the user the re-enter credentials every time. +Enabling this option will decrease performance when using Basic Auth by +requiring the user the re-enter credentials every time. Value|Refresh Credentials Before Returning -|- @@ -401,14 +430,106 @@ export GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS=1 Defaults to false/disabled. -**Also see: [credential.bitbucketAlwaysRefreshCredentials](configuration.md#bitbucketAlwaysRefreshCredentials)** +**Also see: [credential.bitbucketAlwaysRefreshCredentials](configuration.md#credentialbitbucketAlwaysRefreshCredentials)** + +--- + +### GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS + +Forces GCM to validate any stored credentials before returning them to Git. It +does this by calling a REST API resource that requires authentication. + +Disabling this option reduces the HTTP traffic within GCM when it is retrieving +credentials. This may improve user performance, but will increase the number of +times Git remote calls fail to authenticate with the host and therefore require +the user to re-try the Git remote call. + +Enabling this option helps ensure Git is always provided with valid credentials. + +Value|Validate credentials +-|- +`true`, `1`, `yes`, `on`_(default)_|Always +`false`, `0`, `no`, `off`|Never + +#### Windows + +```batch +SET GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS=1 +``` + +#### macOS/Linux + +```bash +export GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS=1 +``` + +Defaults to true/enabled. + +**Also see: [credential.bitbucketValidateStoredCredentials](configuration.md#credentialbitbucketValidateStoredCredentials)** + +--- + +### GCM_BITBUCKET_DATACENTER_CLIENTID + +To use OAuth with Bitbucket DC it is necessary to create an external, incoming +[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html). + +It is then necessary to configure the local GCM installation with the OAuth +[ClientId](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTID) and +[ClientSecret](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTSECRET) +from the AppLink. + +#### Windows + +```batch +SET GCM_BITBUCKET_DATACENTER_CLIENTID=1111111111111111111 +``` + +#### macOS/Linux + +```bash +export GCM_BITBUCKET_DATACENTER_CLIENTID=1111111111111111111 +``` + +Defaults to undefined. + +**Also see: [credential.bitbucketDataCenterOAuthClientId](configuration.md#credentialbitbucketDataCenterOAuthClientId)** + +--- + +### GCM_BITBUCKET_DATACENTER_CLIENTSECRET + +To use OAuth with Bitbucket DC it is necessary to create an external, incoming +[AppLink](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html). + +It is then necessary to configure the local GCM installation with the OAuth +[ClientId](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTID) and +[ClientSecret](environment.md#GCM_BITBUCKET_DATACENTER_CLIENTSECRET) +from the AppLink. + +#### Windows + +```batch +SET GCM_BITBUCKET_DATACENTER_CLIENTSECRET=222222222222222222222 +``` + +#### macOS/Linux + +```bash +export GCM_BITBUCKET_DATACENTER_CLIENTSECRET=222222222222222222222 +``` + +Defaults to undefined. + +**Also see: [credential.bitbucketDataCenterOAuthClientSecret](configuration.md#credentialbitbucketDataCenterOAuthClientSecret)** --- ### GCM_GITHUB_AUTHMODES -Override the available authentication modes presented during GitHub authentication. -If this option is not set, then the available authentication modes will be automatically detected. +Override the available authentication modes presented during GitHub +authentication. If this option is not set, then the available authentication +modes will be automatically detected. **Note:** This setting supports multiple values separated by commas. @@ -433,14 +554,15 @@ SET GCM_GITHUB_AUTHMODES="oauth,basic" export GCM_GITHUB_AUTHMODES="oauth,basic" ``` -**Also see: [credential.gitHubAuthModes](configuration.md#credentialgitHubAuthModes)** +**Also see: [credential.gitHubAuthModes][credential-githubauthmodes]** --- ### GCM_GITLAB_AUTHMODES -Override the available authentication modes presented during GitLab authentication. -If this option is not set, then the available authentication modes will be automatically detected. +Override the available authentication modes presented during GitLab +authentication. If this option is not set, then the available authentication +modes will be automatically detected. **Note:** This setting supports multiple values separated by commas. @@ -463,14 +585,15 @@ SET GCM_GITLAB_AUTHMODES="browser" export GCM_GITLAB_AUTHMODES="browser" ``` -**Also see: [credential.gitLabAuthModes](configuration.md#credentialgitLabAuthModes)** +**Also see: [credential.gitLabAuthModes][credential-gitlabauthmodes]** --- ### GCM_NAMESPACE -Use a custom namespace prefix for credentials read and written in the OS credential store. -Credentials will be stored in the format `{namespace}:{service}`. +Use a custom namespace prefix for credentials read and written in the OS +credential store. Credentials will be stored in the format +`{namespace}:{service}`. Defaults to the value `git`. @@ -486,7 +609,7 @@ SET GCM_NAMESPACE="my-namespace" export GCM_NAMESPACE="my-namespace" ``` -**Also see: [credential.namespace](configuration.md#credentialnamespace)** +**Also see: [credential.namespace][credential-namespace]** --- @@ -494,20 +617,22 @@ export GCM_NAMESPACE="my-namespace" Select the type of credential store to use on supported platforms. -Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset on Linux. +Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset +on Linux. -**Note:** See more information about configuring secret stores [here](credstores.md). +**Note:** For more information about configuring secret stores see the +[credential stores documentation][credential-stores]. Value|Credential Store|Platforms -|-|- _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|- `wincredman`|Windows Credential Manager (not available over SSH).|Windows -`dpapi`|DPAPI protected files. Customize the DPAPI store location with [`GCM_DPAPI_STORE_PATH`](#gcm_dpapi_store_path)|Windows +`dpapi`|DPAPI protected files. Customize the DPAPI store location with [`GCM_DPAPI_STORE_PATH`][gcm-dpapi-store-path]|Windows `keychain`|macOS Keychain.|macOS -`secretservice`|[freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/) via [libsecret](https://wiki.gnome.org/Projects/Libsecret) (requires a graphical interface to unlock secret collections).|Linux -`gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility](https://www.passwordstore.org/) (requires GPG and `pass` to initialize the store).|macOS, Linux -`cache`|Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache).|Windows, macOS, Linux -`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`GCM_PLAINTEXT_STORE_PATH`](#gcm_plaintext_store_path).|Windows, macOS, Linux +`secretservice`|[freedesktop.org Secret Service API][freedesktop-ss] via [libsecret][libsecret] (requires a graphical interface to unlock secret collections).|Linux +`gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility][passwordstore] (requires GPG and `pass` to initialize the store).|macOS, Linux +`cache`|Git's built-in [credential cache][git-credential-cache].|Windows, macOS, Linux +`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`GCM_PLAINTEXT_STORE_PATH`][gcm-plaintext-store-path].|Windows, macOS, Linux #### Windows @@ -521,14 +646,14 @@ SET GCM_CREDENTIAL_STORE="gpg" export GCM_CREDENTIAL_STORE="gpg" ``` -**Also see: [credential.credentialStore](configuration.md#credentialcredentialstore)** +**Also see: [credential.credentialStore][credential-credentialstore]** --- ### GCM_CREDENTIAL_CACHE_OPTIONS -Pass [options](https://git-scm.com/docs/git-credential-cache#_options) -to the Git credential cache when [`GCM_CREDENTIAL_STORE`](#GCM_CREDENTIAL_STORE) +Pass [options][git-cache-options] +to the Git credential cache when [`GCM_CREDENTIAL_STORE`][gcm-credential-store] is set to `cache`. This allows you to select a different amount of time to cache credentials (the default is 900 seconds) by passing `"--timeout "`. Use of other options like `--socket` is untested @@ -548,13 +673,14 @@ SET GCM_CREDENTIAL_CACHE_OPTIONS="--timeout 300" export GCM_CREDENTIAL_CACHE_OPTIONS="--timeout 300" ``` -**Also see: [credential.cacheOptions](configuration.md#credentialcacheoptions)** +**Also see: [credential.cacheOptions][credential-cacheoptions]** --- ### GCM_PLAINTEXT_STORE_PATH -Specify a custom directory to store plaintext credential files in when [`GCM_CREDENTIAL_STORE`](#GCM_CREDENTIAL_STORE) is set to `plaintext`. +Specify a custom directory to store plaintext credential files in when +[`GCM_CREDENTIAL_STORE`][gcm-credential-store] is set to `plaintext`. Defaults to the value `~/.gcm/store` or `%USERPROFILE%\.gcm\store`. @@ -570,13 +696,14 @@ SETX GCM_PLAINTEXT_STORE_PATH=D:\credentials export GCM_PLAINTEXT_STORE_PATH=/mnt/external-drive/credentials ``` -**Also see: [credential.plaintextStorePath](configuration.md#credentialplaintextstorepath)** +**Also see: [credential.plaintextStorePath][credential-plain-text-store]** --- ### GCM_DPAPI_STORE_PATH -Specify a custom directory to store DPAPI protected credential files in when [`GCM_CREDENTIAL_STORE`](#GCM_CREDENTIAL_STORE) is set to `dpapi`. +Specify a custom directory to store DPAPI protected credential files in when +[`GCM_CREDENTIAL_STORE`][gcm-credential-store] is set to `dpapi`. Defaults to the value `%USERPROFILE%\.gcm\dpapi_store`. @@ -586,15 +713,19 @@ Defaults to the value `%USERPROFILE%\.gcm\dpapi_store`. SETX GCM_DPAPI_STORE_PATH=D:\credentials ``` -**Also see: [credential.dpapiStorePath](configuration.md#credentialdpapistorepath)** +**Also see: [credential.dpapiStorePath][credential-dpapi-store-path]** --- ### GCM_GPG_PATH -Specify the path (_including_ the executable name) to the version of `gpg` used by `pass` (`gpg2` if present, otherwise `gpg`). This is primarily meant to allow manual resolution of the conflict that occurs on legacy Linux systems with parallel installs of `gpg` and `gpg2`. +Specify the path (_including_ the executable name) to the version of `gpg` used +by `pass` (`gpg2` if present, otherwise `gpg`). This is primarily meant to allow +manual resolution of the conflict that occurs on legacy Linux systems with +parallel installs of `gpg` and `gpg2`. -If not specified, GCM defaults to using the version of `gpg2` on the `$PATH`, falling back on `gpg` if `gpg2` is not found. +If not specified, GCM defaults to using the version of `gpg2` on the `$PATH`, +falling back on `gpg` if `gpg2` is not found. #### macOS/Linux @@ -608,11 +739,12 @@ _No configuration equivalent._ ### GCM_MSAUTH_FLOW -Specify which authentication flow should be used when performing Microsoft authentication and an interactive flow is required. +Specify which authentication flow should be used when performing Microsoft +authentication and an interactive flow is required. Defaults to `auto`. -**Note:** If [`GCM_MSAUTH_USEBROKER`](#gcm_msauth_usebroker-experimental) is set to `true` +**Note:** If [`GCM_MSAUTH_USEBROKER`][gcm-msauth-usebroker] is set to `true` and the operating system authentication broker is available, all flows will be delegated to the broker. If both of those things are true, then the value of `GCM_MSAUTH_FLOW` has no effect. @@ -636,7 +768,7 @@ SET GCM_MSAUTH_FLOW="devicecode" export GCM_MSAUTH_FLOW="devicecode" ``` -**Also see: [credential.msauthFlow](configuration.md#credentialmsauthflow)** +**Also see: [credential.msauthFlow][credential-msauth-flow]** --- @@ -646,7 +778,9 @@ Use the operating system account manager where available. Defaults to `false`. This default is subject to change in the future. -_**Note:** before you enable this option on Windows, please [review the details](windows-broker.md) about what this means to your local Windows user account._ +_**Note:** before you enable this option on Windows, please +[review the details][windows-broker] about what this means to your local Windows +user account._ Value|Description -|- @@ -665,7 +799,7 @@ SET GCM_MSAUTH_USEBROKER="true" export GCM_MSAUTH_USEBROKER="false" ``` -**Also see: [credential.msauthUseBroker](configuration.md#credentialmsauthusebroker-experimental)** +**Also see: [credential.msauthUseBroker][credential-msauth-usebroker]** --- @@ -680,7 +814,7 @@ Value|Description `pat` _(default)_|Azure DevOps personal access tokens `oauth`|Microsoft identity OAuth tokens (AAD or MSA tokens) -More information about Azure Access tokens can be found [here](azrepos-users-and-tokens.md). +More information about Azure Access tokens can be found [here][azure-access-tokens]. #### Windows @@ -694,4 +828,43 @@ SET GCM_AZREPOS_CREDENTIALTYPE="oauth" export GCM_AZREPOS_CREDENTIALTYPE="oauth" ``` -**Also see: [credential.azreposCredentialType](configuration.md#azreposcredentialtype)** +**Also see: [credential.azreposCredentialType][credential-azrepos-credential-type]** + +[autodetect]: autodetect.md +[azure-access-tokens]: azrepos-users-and-tokens.md +[configuration]: configuration.md +[credential-allowwindowsauth]: environment.md#credentialallowWindowsAuth +[credential-authority]: configuration.md#credentialauthority-deprecated +[credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout +[credential-azrepos-credential-type]: configuration.md#azreposcredentialtype +[credential-bitbucketauthmodes]: configuration.md#credentialbitbucketAuthModes +[credential-cacheoptions]: configuration.md#credentialcacheoptions +[credential-credentialstore]: configuration.md#credentialcredentialstore +[credential-dpapi-store-path]: configuration.md#credentialdpapistorepath +[credential-githubauthmodes]: configuration.md#credentialgitHubAuthModes +[credential-gitlabauthmodes]: configuration.md#credentialgitLabAuthModes +[credential-guiprompt]: configuration.md#credentialguiprompt +[credential-httpproxy]: configuration.md#credentialhttpProxy-deprecated +[credential-interactive]: configuration.md#credentialinteractive +[credential-namespace]: configuration.md#credentialnamespace +[credential-msauth-flow]: configuration.md#credentialmsauthflow +[credential-msauth-usebroker]: configuration.md#credentialmsauthusebroker-experimental +[credential-plain-text-store]: configuration.md#credentialplaintextstorepath +[credential-provider]: configuration.md#credentialprovider +[credential-stores]: credstores.md +[default-values]: enterprise-config.md +[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/ +[gcm]: usage.md +[gcm-interactive]: #gcm_interactive +[gcm-credential-store]: #gcm_credential_store +[gcm-dpapi-store-path]: #gcm_dpapi_store_path +[gcm-plaintext-store-path]: #gcm_plaintext_store_path +[gcm-msauth-usebroker]: #gcm_msauth_usebroker-experimental +[git-cache-options]: https://git-scm.com/docs/git-credential-cache#_options +[git-credential-cache]: https://git-scm.com/docs/git-credential-cache +[git-httpproxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy +[network-http-proxy]: netconfig.md#http-proxy +[libsecret]: https://wiki.gnome.org/Projects/Libsecret +[migration-guide]: migration.md#gcm_authority +[passwordstore]: https://www.passwordstore.org/ +[windows-broker]: windows-broker.md diff --git a/docs/faq.md b/docs/faq.md index b61d43d0e..7008b793b 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -6,58 +6,85 @@ Please follow these steps to diagnose or resolve the problem: -1. Check if you can access the remote repository in a web browser. If you cannot, this is probably a permission problem and you should follow up with the repository administrator for access. Execute `git remote -v` from a terminal to show the remote URL. +1. Check if you can access the remote repository in a web browser. If you +cannot, this is probably a permission problem and you should follow up with the +repository administrator for access. Execute `git remote -v` from a terminal to +show the remote URL. -1. If you are experiencing a Git authentication problem using an editor, IDE or other tool, try performing the same operation from the terminal. Does this still fail? If the operation succeeds from the terminal please include details of the specific tool and version in any issue reports. +1. If you are experiencing a Git authentication problem using an editor, IDE or +other tool, try performing the same operation from the terminal. Does this still +fail? If the operation succeeds from the terminal please include details of the +specific tool and version in any issue reports. -1. Set the environment variable `GCM_TRACE` and run the Git operation again. Find instructions [here](environment.md#GCM_TRACE). +1. Set the environment variable `GCM_TRACE` and run the Git operation again. +Find instructions in the [environment doc][env-trace]. -1. If all else fails, create an issue [here](https://github.com/GitCredentialManager/git-credential-manager/issues/create), making sure to include the trace log. +1. If all else fails, create an issue [here][create-issue], making sure to +include the trace log. ### Q: I got an error saying unsecure HTTP is not supported -To keep your data secure, Git Credential Manager will not send credentials for Azure Repos, Azure DevOps Server (TFS), GitHub, and Bitbucket, over HTTP connections that are not secured using TLS (HTTPS). +To keep your data secure, Git Credential Manager will not send credentials for +Azure Repos, Azure DevOps Server (TFS), GitHub, and Bitbucket, over HTTP +connections that are not secured using TLS (HTTPS). Please make sure your remote URLs use "https://" rather than "http://". ### Q: I got an authentication error and I am behind a network proxy -You probably need to configure Git and GCM to use a proxy. Please see detailed information [here](https://aka.ms/gcm/httpproxy). +You probably need to configure Git and GCM to use a proxy. Please see detailed +information in the [network config doc][netconfig-http-proxy]. ### Q: I'm getting errors about picking a credential store on Linux -On Linux you must [select and configure a credential store](https://aka.ms/gcm/credstores), as due to the varied nature of distributions and installations, we cannot guarantee a suitable storage solution is available. +On Linux you must [select and configure a credential store][credstores], as due +to the varied nature of distributions and installations, we cannot guarantee a +suitable storage solution is available. ## About the project -### Q: How does this project relate to [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows) and [Git Credential Manager for Mac and Linux](https://github.com/Microsoft/Git-Credential-Manager-for-Mac-and-Linux)? +### Q: How does this project relate to [Git Credential Manager for Windows][gcm-windows] and [Git Credential Manager for Mac and Linux][gcm-linux]? -Git Credential Manager for Windows (GCM Windows) is a .NET Framework-based Git credential helper which runs on Windows. -Likewise the Git Credential Manager for Mac and Linux (Java GCM) is a Java-based Git credential helper that runs only on macOS and Linux. Although both of these projects aim to solve the same problem (providing seamless multi-factor HTTPS authentication with Git), they are based on different codebases and languages which is becoming hard to manage to ensure feature parity. +Git Credential Manager for Windows (GCM Windows) is a .NET Framework-based Git +credential helper which runs on Windows. Likewise the Git Credential Manager for +Mac and Linux (Java GCM) is a Java-based Git credential helper that runs only on +macOS and Linux. Although both of these projects aim to solve the same problem +(providing seamless multi-factor HTTPS authentication with Git), they are based +on different codebases and languages which is becoming hard to manage to ensure +feature parity. -Git Credential Manager (GCM; this project) aims to replace both GCM Windows and Java GCM with a unified codebase which should be easier to maintain and enhance in the future. +Git Credential Manager (GCM; this project) aims to replace both GCM Windows and +Java GCM with a unified codebase which should be easier to maintain and enhance +in the future. ### Q: Does this mean GCM for Windows (.NET Framework-based) is deprecated? -Yes. Git Credential Manager for Windows (GCM Windows) is no longer receiving updates and fixes. All development effort has now been directed to GCM. GCM is available as an credential helper option in Git for Windows 2.28, and will be made the default helper in 2.29. +Yes. Git Credential Manager for Windows (GCM Windows) is no longer receiving +updates and fixes. All development effort has now been directed to GCM. GCM is +available as an credential helper option in Git for Windows 2.28, and will be +made the default helper in 2.29. ### Q: Does this mean the Java-based GCM for Mac/Linux is deprecated? -Yes. Usage of Git Credential Manager for Mac and Linux (Java GCM) should be replaced with GCM or SSH keys. If you wish to install GCM on macOS or Linux, please follow the [download and installation instructions](../README.md#download-and-install). +Yes. Usage of Git Credential Manager for Mac and Linux (Java GCM) should be +replaced with GCM or SSH keys. If you wish to install GCM on macOS or Linux, +please follow the [download and installation instructions][download-and-install]. ### Q: I want to use SSH -GCM is only useful for HTTP(S)-based remotes. Git supports SSH out-of-the box so you shouldn't need to install anything else. +GCM is only useful for HTTP(S)-based remotes. Git supports SSH out-of-the box so +you shouldn't need to install anything else. To use SSH please follow the below links: -- [Azure DevOps](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops) -- [GitHub](https://help.github.com/en/articles/connecting-to-github-with-ssh) -- [Bitbucket](https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html) +- [Azure DevOps][azure-ssh] +- [GitHub][github-ssh] +- [Bitbucket][bitbucket-ssh] ### Q: Are HTTP(S) remotes preferred over SSH? -No, neither are "preferred". SSH isn't going away, and is supported "natively" in Git. +No, neither are "preferred". SSH isn't going away, and is supported "natively" +in Git. ### Q: Why did you not just port the existing GCM Windows codebase from .NET Framework to .NET Core? @@ -65,31 +92,53 @@ GCM Windows was not designed with a cross-platform architecture. ### What level of support does GCM have? -Support will be best-effort. We would really appreciate your feedback to make this a great experience across each platform we support. +Support will be best-effort. We would really appreciate your feedback to make +this a great experience across each platform we support. ### Q: Why does GCM not support operating system/distribution 'X', or Git hosting provider 'Y'? The likely answer is we haven't gotten around to that yet! 🙂 -We are working on ensuring support for the Windows, macOS, and Ubuntu operating system, as well as the following Git hosting providers: Azure Repos, Azure DevOps Server (TFS), GitHub, and Bitbucket. +We are working on ensuring support for the Windows, macOS, and Ubuntu operating +system, as well as the following Git hosting providers: Azure Repos, Azure +DevOps Server (TFS), GitHub, and Bitbucket. -We are happy to accept proposals and/or contributions to enable GCM to run on other platforms and Git host providers. Thank you! +We are happy to accept proposals and/or contributions to enable GCM to run on +other platforms and Git host providers. Thank you! ## Technical ### Why is the `credential.useHttpPath` setting required for `dev.azure.com`? -Due to the design of Git and credential helpers such as GCM, we need this setting to make Git use the full remote URL (including the path component) when communicating with GCM. The new `dev.azure.com` format of Azure DevOps URLs means the account name is now part of the path component (for example: `https://dev.azure.com/contoso/...`). The Azure DevOps account name is required in order to resolve the correct authority for authentication (which Azure AD tenant backs this account, or if it is backed by Microsoft personal accounts). - -In the older GCM for Windows product, the solution to the same problem was a "hack". GCM for Windows would walk the process tree looking for the `git-remote-https.exe` process, and attempt to read/parse the process environment block looking for the command line arguments (that contained the full remote URL). This is fragile and not a cross-platform solution, hense the need for the `credential.useHttpPath` setting with GCM. +Due to the design of Git and credential helpers such as GCM, we need this +setting to make Git use the full remote URL (including the path component) when +communicating with GCM. The new `dev.azure.com` format of Azure DevOps URLs +means the account name is now part of the path component (for example: +`https://dev.azure.com/contoso/...`). The Azure DevOps account name is required +in order to resolve the correct authority for authentication (which Azure AD +tenant backs this account, or if it is backed by Microsoft personal accounts). + +In the older GCM for Windows product, the solution to the same problem was a +"hack". GCM for Windows would walk the process tree looking for the +`git-remote-https.exe` process, and attempt to read/parse the process +environment block looking for the command line arguments (that contained the +full remote URL). This is fragile and not a cross-platform solution, hence the +need for the `credential.useHttpPath` setting with GCM. ### Why does GCM take so long at startup the first time? -GCM will [autodetect](autodetect.md) what kind of Git host it's talking to. GitHub, Bitbucket, and Azure DevOps each have their own form(s) of authentication, plus there's a "generic" username and password option. +GCM will [autodetect][autodetect] what kind of Git host it's talking to. GitHub, +Bitbucket, and Azure DevOps each have their own form(s) of authentication, plus +there's a "generic" username and password option. -For the hosted versions of these services, GCM can guess from the URL which service to use. But for on-premises versions which would have unique URLs, GCM will probe with a network call. GCM caches the results of the probe, so it should be faster on the second and later invocations. +For the hosted versions of these services, GCM can guess from the URL which +service to use. But for on-premises versions which would have unique URLs, GCM +will probe with a network call. GCM caches the results of the probe, so it +should be faster on the second and later invocations. -If you know which provider you're talking to and want to avoid the probe, that's possible. You can explicitly tell GCM which provider to use for a URL "example.com" like this: +If you know which provider you're talking to and want to avoid the probe, that's +possible. You can explicitly tell GCM which provider to use for a URL +"example.com" like this: Provider|Command -|- @@ -100,24 +149,33 @@ Generic|`git config --global credential.https://example.com.provider generic` ### How do I fix "Could not create SSL/TLS secure channel" errors on Windows 7? -This likely indicates that you don't have newer TLS versions available. Please [follow Microsoft's guide](https://support.microsoft.com/topic/update-to-enable-tls-1-1-and-tls-1-2-as-default-secure-protocols-in-winhttp-in-windows-c4bd73d2-31d7-761e-0178-11268bb10392) for enabling TLS 1.1 and 1.2 on your machine, specifically the **SChannel** instructions. You'll need to be on at least Windows 7 SP1, and in the end you should have a `TLS 1.2` key with `DisabledByDefault` set to `0`. You can also read [more from Microsoft](https://docs.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn786418(v=ws.11)#tls-12) on this change. +This likely indicates that you don't have newer TLS versions available. Please +[follow Microsoft's guide][enable-windows-ssh] for enabling TLS 1.1 and 1.2 on +your machine, specifically the **SChannel** instructions. You'll need to be on +at least Windows 7 SP1, and in the end you should have a `TLS 1.2` key with +`DisabledByDefault` set to `0`. You can also read +[more from Microsoft][windows-server-tls] on this change. ### How do I use GCM with Windows Subsystem for Linux (WSL)? -Follow the instructions in [our WSL guide](wsl.md) carefully. Especially note the need to run `git config --global credential.https://dev.azure.com.useHttpPath true` _within_ WSL if you're using Azure DevOps. +Follow the instructions in [our WSL guide][wsl] carefully. Especially note the +need to run `git config --global credential.https://dev.azure.com.useHttpPath true` +_within_ WSL if you're using Azure DevOps. ### Does GCM work with multiple users? If so, how? -That's a fairly complicated question to answer, but in short, yes. See [our document on multiple users](multiple-users.md) for details. +That's a fairly complicated question to answer, but in short, yes. See +[our document on multiple users][multiple-users] for details. ### How can I disable GUI dialogs and prompts? There are various environment variables and configuration options available to customize how GCM will prompt you (or not) for input. Please see the following: -- [`GCM_INTERACTIVE`](environment.md#GCM_INTERACTIVE) / [`credential.interactive`](configuration.md#credentialinteractive) -- [`GCM_GUI_PROMPT`](environment.md#GCM_GUI_PROMPT) / [`credential.guiPrompt`](configuration.md#credentialguiprompt) -- [`GIT_TERMINAL_PROMPT`](https://git-scm.com/docs/git#Documentation/git.txt-codeGITTERMINALPROMPTcode) (note this is a _Git setting_ that will affect Git as well as GCM) +- [`GCM_INTERACTIVE`][env-interactive] / [`credential.interactive`][config-interactive] +- [`GCM_GUI_PROMPT`][env-gui-prompt] / [`credential.guiPrompt`][config-gui-prompt] +- [`GIT_TERMINAL_PROMPT`][git-term-prompt] (note this is a _Git setting_ that +will affect Git as well as GCM) ### How can I extend GUI prompts/integrate prompts with my application? @@ -140,27 +198,61 @@ text-based prompts instead. ### How do I revoke consent for GCM for GitHub.com? In your GitHub user settings, navigate to -[Integrations > Applications > Authorized OAuth Apps > Git Credential Manager](https://github.com/settings/connections/applications/0120e057bd645470c1ed) +[Integrations > Applications > Authorized OAuth Apps > Git Credential Manager][github-connected-apps] and pick "Revoke access". -![Revoke GCM OAuth app access](img/github-oauthapp-revoke.png) +![Revoke GCM OAuth app access][github-oauthapp-revoke] -After revoking access, any tokens created by GCM will be invalidated and can no longer be used to access your repositories. The next time GCM attempts to access GitHub.com you will be prompted to consent again. +After revoking access, any tokens created by GCM will be invalidated and can no +longer be used to access your repositories. The next time GCM attempts to access +GitHub.com you will be prompted to consent again. ### I used the install from source script to install GCM on my Linux distribution. Now how can I uninstall GCM and its dependencies? -Please see full instructions [here](./linux-fromsrc-uninstall.md). +Please see full instructions [here][linux-uninstall-from-src]. ### How do I revoke access for a GitLab OAuth application? -There are some scenarios (e.g. updated scopes) for which you will need to manually revoke and re-authorize access for a GitLab OAuth application. You can do so by: +There are some scenarios (e.g. updated scopes) for which you will need to +manually revoke and re-authorize access for a GitLab OAuth application. You can +do so by: -1. Navigating to [the **Applications** page within your **User Settings**](https://gitlab.com/-/profile/applications). +1. Navigating to [the **Applications** page within your **User Settings**][gitlab-apps]. 2. Scrolling to **Authorized applications**. -3. Clicking the **Revoke** button next to the name of the application for which you would like to revoke access (Git Credential Manager is used here for demonstration purposes). +3. Clicking the **Revoke** button next to the name of the application for which +you would like to revoke access (Git Credential Manager is used here for +demonstration purposes). - ![Button to revoke GitLab OAuth Application access](./img/gitlab-oauthapp-revoke.png) + ![Button to revoke GitLab OAuth Application access][gitlab-oauthapp-revoke] 4. Waiting for a notification stating **The application was revoked access**. - ![Notifaction of successful revocation](./img/gitlab-oauthapp-revoked.png) -5. Re-authorizing the application with the new scope (GCM should automatically initiate this flow for you next time access is requested). + ![Notifaction of successful revocation][gitlab-oauthapp-revoked] +5. Re-authorizing the application with the new scope (GCM should automatically +initiate this flow for you next time access is requested). + +[autodetect]: autodetect.md +[azure-ssh]: https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops +[bitbucket-ssh]: https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html +[config-gui-prompt]: configuration.md#credentialguiprompt +[config-interactive]: configuration.md#credentialinteractive +[create-issue]: https://github.com/GitCredentialManager/git-credential-manager/issues/create +[credstores]: credstores.md +[download-and-install]: ../README.md#download-and-install +[enable-windows-ssh]: https://support.microsoft.com/topic/update-to-enable-tls-1-1-and-tls-1-2-as-default-secure-protocols-in-winhttp-in-windows-c4bd73d2-31d7-761e-0178-11268bb10392 +[env-gui-prompt]: environment.md#GCM_GUI_PROMPT +[env-interactive]: environment.md#GCM_INTERACTIVE +[env-trace]: environment.md#GCM_TRACE +[gcm-linux]: https://github.com/Microsoft/Git-Credential-Manager-for-Mac-and-Linux +[gcm-windows]: https://github.com/Microsoft/Git-Credential-Manager-for-Windows +[git-term-prompt]: https://git-scm.com/docs/git#Documentation/git.txt-codeGITTERMINALPROMPTcode +[github-connected-apps]: https://github.com/settings/connections/applications/0120e057bd645470c1ed +[github-oauthapp-revoke]: img/github-oauthapp-revoke.png +[github-ssh]: https://help.github.com/en/articles/connecting-to-github-with-ssh +[gitlab-apps]: https://gitlab.com/-/profile/applications +[gitlab-oauthapp-revoke]: ./img/gitlab-oauthapp-revoke.png +[gitlab-oauthapp-revoked]: ./img/gitlab-oauthapp-revoked.png +[multiple-users]: multiple-users.md +[netconfig-http-proxy]: netconfig.md#http-proxy +[linux-uninstall-from-src]: ./linux-fromsrc-uninstall.md +[windows-server-tls]: https://docs.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn786418(v=ws.11)#tls-12 +[wsl]: wsl.md diff --git a/docs/github-apideprecation.md b/docs/github-apideprecation.md index 9d5c3b9a3..f9201db0a 100644 --- a/docs/github-apideprecation.md +++ b/docs/github-apideprecation.md @@ -2,14 +2,13 @@ ## What's going on? -GitHub now [requires token-based authentication](https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/) to +GitHub now [requires token-based authentication][token-auth] to call their APIs, and in the future, use Git itself. This means Git credential helpers such as [Git Credential Manager (GCM) for -Windows](https://github.com/microsoft/Git-Credential-Manager-for-Windows), and -old versions of [GCM](https://aka.ms/gcm) that offer username/password -flows **will not be able to create new access tokens** for accessing Git -repositories. +Windows][gcm-windows], and old versions of [GCM][gcm] that offer +username/password flows **will not be able to create new access tokens** for +accessing Git repositories. If you already have tokens generated by Git credential helpers like GCM for Windows, they will continue to work until they expire or are revoked/deleted. @@ -22,15 +21,15 @@ The best thing to do right now is upgrade to the latest Git for Windows (at least version 2.29), which includes a version of Git Credential Manager that uses supported OAuth token-based authentication. -[Download the latest Git for Windows ⬇️](https://git-scm.com/download/win) +[Download the latest Git for Windows ⬇️][git-windows] ### Visual Studio users Please update to the latest supported release of Visual Studio, that includes GCM and support for OAuth token-based authentication. -- [Visual Studio 2019 ⬇️](https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2019) -- [Visual Studio 2017 ⬇️](https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2017) +- [Visual Studio 2019 ⬇️][vs-2019] +- [Visual Studio 2017 ⬇️][vs-2017] ### SSH, macOS, and Linux users @@ -38,7 +37,7 @@ If you are using SSH this change does **not** affect you. If you are using an older version of Git Credential Manager (before 2.0.124-beta) please upgrade to the latest version following [these -instructions](https://github.com/GitCredentialManager/git-credential-manager#download-and-install). +instructions][gcm-install]. ## What if I cannot upgrade Git for Windows? @@ -46,18 +45,18 @@ If you are unable to upgrade Git for Windows, you can manually install Git Credential Manager as a standalone install. This will override the older, GCM for Windows bundled with the Git for Windows installation. -[Download Git Credential Manager standalone ⬇️](https://aka.ms/gcm/latest) +[Download Git Credential Manager standalone ⬇️][gcm-latest] ## What if I cannot use Git Credential Manager? If you are unable to use Git Credential Manager due to a bug or -compatibility issue we'd [like to know why](https://github.com/GitCredentialManager/git-credential-manager/issues/new/choose)! +compatibility issue we'd [like to know why][gcm-new-issue]! ## Help! I cannot make any changes to my Windows machine without an Administrator If you do not have permission to change your installation (for example in a corporate environment) you can use the per-user installer. Check out the [latest -release](https://aka.ms/gcm/latest) and download the `gcmcoreuser-win-*.exe` +release][gcm-latest] and download the `gcmcoreuser-win-*.exe` executable. ### Help! I still cannot or don't want to install anything @@ -67,26 +66,27 @@ There is a workaround which should work and doesn't require installing anything. 1. Tell your system administrator they should start planning to upgrade the installed version of Git for Windows to at least 2.29! 😁 -1. [Create a new personal access token](https://github.com/settings/tokens/new?scopes=repo,gist,workflow) (see official [documentation](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)) +1. [Create a new personal access token][github-pat] (see official + [documentation][github-pat-docs]) 1. Enter a name ("note") for the token and ensure the `repo`, `gist`, and `workflow` scopes are selected: - ![image](https://user-images.githubusercontent.com/5658207/95448332-1beb2000-095b-11eb-9a48-9c05b1926a6b.png) + ![image][github-pat-note-image] ... - ![image](https://user-images.githubusercontent.com/5658207/95447304-6f5c6e80-0959-11eb-924b-50b86c2b3d77.png) + ![image][github-pat-repo-scope-image] ... - ![image](https://user-images.githubusercontent.com/5658207/95447450-a3d02a80-0959-11eb-82a8-2d2834d5aa16.png) + ![image][github-pat-gist-scope-image] ... - ![image](https://user-images.githubusercontent.com/5658207/95447343-7b483080-0959-11eb-8e00-151d53893f3f.png) + ![image][github-pat-workflow-scope-image] 1. Click "Generate Token" - ![image](https://user-images.githubusercontent.com/5658207/95448393-31f8e080-095b-11eb-9568-cfd1c567a65c.png) + ![image][github-generate-pat-image] 1. **[IMPORTANT]** Keep the resulting page open as this contains your new token (this will only be displayed once!) - ![image](https://user-images.githubusercontent.com/5658207/95448288-ff4ee800-095a-11eb-9709-8e37bde8b716.png) + ![image][github-display-pat-image] 1. Save the generated PAT in the Windows Credential Manager: @@ -100,13 +100,13 @@ There is a workaround which should work and doesn't require installing anything. You will be prompted to enter a password – copy the newly generated PAT in step 4 and paste it here, and press the `Enter` key - ![image](https://user-images.githubusercontent.com/5658207/95448479-4fc64580-095b-11eb-9970-0b6faf7f4ae7.png) + ![image][windows-cli-save-pat-image] 1. If you do not wish to use the command-line, [open the Credential Manager - via Control Panel](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) + via Control Panel][windows-credential-manager] and select the "Windows Credentials" tab. - ![image](https://user-images.githubusercontent.com/5658207/96468389-f6e09200-1223-11eb-9993-ae7b4096b769.png) + ![image][windows-gui-credentials-image] Click "Add a generic credential", and enter the following details: @@ -114,12 +114,35 @@ There is a workaround which should work and doesn't require installing anything. - Username: `PersonalAccessToken` - Password: _(copy and paste the PAT generated in step 4 here)_ - ![image](https://user-images.githubusercontent.com/5658207/96468318-ddd7e100-1223-11eb-8cd4-aa118493c538.png) + ![image][windows-gui-add-pat-image] ## What about GitHub Enterprise Server (GHES)? -As mentioned in [the blog post](https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/), +As mentioned in [the blog post][github-token-authentication-requirements], the new token-based authentication requirements **DO NOT** apply to GHES: > We have not announced any changes to GitHub Enterprise Server, which remains > unaffected at this time. + +[token-auth]: https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/ +[gcm]: https://aka.ms/gcm +[gcm-install]: ../README.md#download-and-install +[gcm-latest]: https://aka.ms/gcm/latest +[gcm-new-issue]: https://github.com/GitCredentialManager/git-credential-manager/issues/new/choose +[gcm-windows]: https://github.com/microsoft/Git-Credential-Manager-for-Windows +[git-windows]: https://git-scm.com/download/win +[github-display-pat-image]: img/github-display-pat.png +[github-generate-pat-image]: img/github-generate-pat.png +[github-pat]: https://github.com/settings/tokens/new?scopes=repo,gist,workflow +[github-pat-docs]: https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token +[github-pat-gist-scope-image]: img/github-pat-gist-scope.png +[github-pat-note-image]: img/github-pat-note.png +[github-pat-repo-scope-image]: img/github-pat-repo-scope.png +[github-pat-workflow-scope-image]: img/github-pat-workflow-scope.png +[github-token-authentication-requirements]: https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/ +[windows-cli-save-pat-image]: img/windows-cli-save-pat.png +[vs-2019]: https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2019 +[vs-2017]: https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2017 +[windows-credential-manager]: https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0 +[windows-gui-add-pat-image]: img/windows-gui-add-pat.png +[windows-gui-credentials-image]: img/windows-gui-credentials.png diff --git a/docs/gitlab.md b/docs/gitlab.md index 94065398e..58975b6cd 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -1,17 +1,28 @@ # GitLab support -Git Credential Manager supports [gitlab.com](https://gitlab.com) out the box. +Git Credential Manager supports [gitlab.com][gitlab] out the box. ## Using on a another instance -To use on another instance, eg. `https://gitlab.example.com` requires setup and configuration: +To use on another instance, eg. `https://gitlab.example.com` requires setup and +configuration: -1. [Create an OAuth application](https://docs.gitlab.com/ee/integration/oauth_provider.html). This can be at the user, group or instance level. Specify a name and use a redirect URI of `http://127.0.0.1/`. _Unselect_ the 'Confidential' option, and ensure the 'Expire access tokens' option is selected. Set the 'write_repository' and 'read_repository' scopes. -1. Copy the application ID and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientId ` -1. Copy the application secret and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientSecret ` -1. Configure authentication modes to include 'browser' `git config --global credential.https://gitlab.example.com.gitLabAuthModes browser` -1. For good measure, configure `git config --global credential.https://gitlab.example.com.provider gitlab`. This may be necessary to recognise the domain as a GitLab instance. -1. Verify the config is as expected `git config --global --get-urlmatch credential https://gitlab.example.com` +1. [Create an OAuth application][gitlab-oauth]. This can be at the user, group +or instance level. Specify a name and use a redirect URI of `http://127.0.0.1/`. +_Unselect_ the 'Confidential' option. Set the 'write_repository' and +'read_repository' scopes. +1. Copy the application ID and configure +`git config --global credential.https://gitlab.example.com.GitLabDevClientId ` +1. Copy the application secret and configure +`git config --global credential.https://gitlab.example.com.GitLabDevClientSecret +` +1. Configure authentication modes to include 'browser' +`git config --global credential.https://gitlab.example.com.gitLabAuthModes browser` +1. For good measure, configure +`git config --global credential.https://gitlab.example.com.provider gitlab`. +This may be necessary to recognise the domain as a GitLab instance. +1. Verify the config is as expected +`git config --global --get-urlmatch credential https://gitlab.example.com` ### Clearing config @@ -21,6 +32,34 @@ To use on another instance, eg. `https://gitlab.example.com` requires setup and git config --global --unset-all credential.https://gitlab.example.com.provider ``` +### Config for popular instances + +For convenience, here are the config commands for several popular GitLab +instances, provided by community member [hickford](https://github.com/hickford/): + +```console +# https://gitlab.freedesktop.org/ +git config --global credential.https://gitlab.freedesktop.org.gitlabdevclientid 6503d8c5a27187628440d44e0352833a2b49bce540c546c22a3378c8f5b74d45 +git config --global credential.https://gitlab.freedesktop.org.gitlabdevclientsecret 2ae9343a034ff1baadaef1e7ce3197776b00746a02ddf0323bb34aca8bff6dc1 +# https://gitlab.gnome.org/ +git config --global credential.https://gitlab.gnome.org.gitlabdevclientid adf21361d32eddc87bf6baf8366f242dfe07a7d4335b46e8e101303364ccc470 +git config --global credential.https://gitlab.gnome.org.gitlabdevclientsecret cdca4678f64e5b0be9febc0d5e7aab0d81d27696d7adb1cf8022ccefd0a58fc0 +# https://invent.kde.org/ +git config --global credential.https://invent.kde.org.gitlabdevclientid cd7cb4342c7cd83d8c2fcc22c87320f88d0bde14984432ffca07ee24d0bf0699 +git config --global credential.https://invent.kde.org.gitlabdevclientsecret 9cc8440b280c792ac429b3615ae1c8e0702e6b2479056f899d314f05afd94211 +# https://salsa.debian.org/ +git config --global credential.https://salsa.debian.org.gitlabdevclientid 213f5fd32c6a14a0328048c0a77cc12c19138cc165ab957fb83d0add74656f89 +git config --global credential.https://salsa.debian.org.gitlabdevclientsecret 3616b974b59451ecf553f951cb7b8e6e3c91c6d84dd3247dcb0183dac93c2a26 +# https://gitlab.haskell.org/ +git config --global credential.https://gitlab.haskell.org.gitlabdevclientid 57de5eaab72b3dc447fca8c19cea39527a08e82da5377c2d10a8ebb30b08fa5f +git config --global credential.https://gitlab.haskell.org.gitlabdevclientsecret 5170a480da8fb7341e0daac94223d4fff549c702efb2f8873d950bb2b88e434f +# https://code.videolan.org/ +git config --global credential.https://code.videolan.org.gitlabdevclientid f35c379241cc20bf9dffecb47990491b62757db4fb96080cddf2461eacb40375 +git config --global credential.https://code.videolan.org.gitlabdevclientsecret 631558ec973c5ef65b78db9f41103f8247dc68d979c86f051c0fe4389e1995e8 +``` + +See also [issue #677](https://github.com/GitCredentialManager/git-credential-manager/issues/677). + ## Preferences ```console @@ -31,7 +70,8 @@ Select an authentication method for 'https://gitlab.com/': option (enter for default): ``` -If you have a preferred authentication mode, you can specify [credential.gitLabAuthModes](configuration.md#credential.gitLabAuthModes): +If you have a preferred authentication mode, you can specify +[credential.gitLabAuthModes][config-gitlab-auth-modes]: ```console git config --global credential.gitlabauthmodes browser @@ -39,8 +79,19 @@ git config --global credential.gitlabauthmodes browser ## Caveats -Improved support requires changes in GitLab. Please vote for these issues if they affect you: +Improved support requires changes in GitLab. Please vote for these issues if +they affect you: + +1. No support for OAuth device authorization (necessary for machines without web +browser): [GitLab issue 332682][gitlab-issue-332682] +1. Only domains with prefix `gitlab.` are recognised as GitLab remotes: +[GitLab issue 349464][gitlab-issue-349464] +1. Username/password authentication is suggested even if disabled on server: +[GitLab issue 349463][gitlab-issue-349463] -1. No support for OAuth device authorization (necessary for machines without web browser): [GitLab issue 332682](https://gitlab.com/gitlab-org/gitlab/-/issues/332682) -1. Only domains with prefix `gitlab.` are recognised as GitLab remotes: [GitLab issue 349464](https://gitlab.com/gitlab-org/gitlab/-/issues/349464) -1. Username/password authentication is suggested even if disabled on server: [GitLab issue 349463](https://gitlab.com/gitlab-org/gitlab/-/issues/349463) +[config-gitlab-auth-modes]: configuration.md#credential.gitLabAuthModes +[gitlab]: https://gitlab.com +[gitlab-issue-332682]: https://gitlab.com/gitlab-org/gitlab/-/issues/332682 +[gitlab-issue-349464]: https://gitlab.com/gitlab-org/gitlab/-/issues/349464 +[gitlab-issue-349463]: https://gitlab.com/gitlab-org/gitlab/-/issues/349463 +[gitlab-oauth]: https://docs.gitlab.com/ee/integration/oauth_provider.html diff --git a/docs/hostprovider.md b/docs/hostprovider.md index bfd036363..9a6e16412 100644 --- a/docs/hostprovider.md +++ b/docs/hostprovider.md @@ -1,18 +1,5 @@ # Git Credential Manager Host Provider -Property|Value --|- -Author(s)|Matthew John Cheetham ([@mjcheetham](https://github.com/mjcheetham)) -Revision|1.2 -Last updated|2021-01-08 - -## Revision Summary - -- 1.0. Initial revision. -- 1.1. Replaced `GetCredentialKey` with `GetServiceName`. -- 1.2. Added new `IsSupported(HttpResponseMessage)` overload, the optional - `ICommandProvider` interface, and registration priorities. - ## Abstract Git Credential Manger, the cross-platform and cross-host Git credential @@ -20,37 +7,13 @@ helper, can be extended to support any Git hosting service allowing seamless authentication to secured Git repositories by implementing and registering a "host provider". -## Table of Contents - -- [1. Introduction](#1-introduction) - - [1.1. Notational Conventions](#11-notational-conventions) - - [1.2. Abbreviations](#12-abbreviations) -- [2. Implementation](#2-implementation) - - [2.1. Registration](#21-registration) - - [2.1.2. Ordering](#212-ordering) - - [2.2. Handling Requests](#22-handling-requests) - - [2.2.1. Rejecting Requests](#221-rejecting-requests) - - [2.3. Retrieving Credentials](#23-retrieving-credentials) - - [2.3.1 Authentication Prompts](#231-authentication-prompts) - - [2.4. Storing Credentials](#24-storing-credentials) - - [2.5. Erasing Credentials](#25-erasing-credentials) - - [2.6 `HostProvider` base class](#26-hostprovider-base-class) - - [2.6.1 `GetServiceName`](#261-getservicename) - - [2.6.2 `GenerateCredentialAsync`](#262-generatecredentialasync) - - [2.7. External Metadata](#27-external-metadata) -- [3. Helpers](#3-helpers) - - [3.1. Discovery](#31-discovery) -- [4. Error Handling](#4-error-handling) -- [5. Custom Commands](#5-custom-commands) -- [References](#references) - ## 1. Introduction Git Credential Manager (GCM) is a host and platform agnostic Git credential helper application. Support for authenticating to any Git hosting service can be added to GCM by creating a custom "host provider" and registering it within the product. Host providers can be submitted via a pull -request on GitHub at . +request on [the Git Credential Manager repository on GitHub][gcm]. This document outlines the required and expected behaviour of a host provider, and what is required to implement and register one. @@ -60,7 +23,7 @@ and what is required to implement and register one. The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this specification are to be interpreted as described in -[[RFC2119](https://tools.ietf.org/html/rfc2119)]. +[[RFC2119][rfc-2119]]. ### 1.2. Abbreviations @@ -72,7 +35,7 @@ Manager for Windows" is abbreviated to "GCM for Windows" or "GCM Windows". "Git Credential Manager for Mac & Linux" is abbreviated to "GCM for Mac/Linux" or "GCM Mac/Linux". -OAuth2 [[RFC6749](https://tools.ietf.org/html/rfc6749)] "access tokens" are +OAuth2 [[RFC6749][rfc-6749]] "access tokens" are abbreviated to "ATs" and "refresh tokens" to "RTs". "Personal Access Tokens" are abbreviated to "PATs". @@ -85,7 +48,7 @@ provider with the application via the host provider registry. Host providers MUST implement the `IHostProvider` interface. They can choose to directly implement the interface they MAY derive from the `HostProvider` abstract class (which itself implements the `IHostProvider` interface) - see -[§2.6](#26-hostprovider-base-class). +[2.6][hostprovider-base-class]. Implementors MUST implement all interface properties and abstract methods. @@ -371,11 +334,18 @@ If a host provider wishes to surface custom commands the SHOULD implement the Each provider is given the opportunity to create a single `ProviderCommand` instance to which further sub-commands can be parented to. Commanding is -provided by the `System.CommandLine` API library [[1](#references)]. +provided by the `System.CommandLine` API library [[1][references]]. There are no limitations on what format sub-commands, arguments, or options must take, but implementors SHOULD attempt to follow existing practices and styles. ## References -1. `System.CommandLine` API () +1. [`System.CommandLine` API][github-dotnet-cli] + +[gcm]: https://github.com/GitCredentialManager/git-credential-manager +[github-dotnet-cli]: https://github.com/dotnet/command-line-api +[hostprovider-base-class]: #26-hostprovider-base-class +[references]: #references +[rfc-2119]: https://www.rfc-editor.org/rfc/rfc2119 +[rfc-6749]: https://www.rfc-editor.org/rfc/rfc6749 diff --git a/docs/img/gcmcore-rename.png b/docs/img/gcmcore-rename.png new file mode 100644 index 000000000..8b986f473 Binary files /dev/null and b/docs/img/gcmcore-rename.png differ diff --git a/docs/img/github-display-pat.png b/docs/img/github-display-pat.png new file mode 100644 index 000000000..c1d4818d0 Binary files /dev/null and b/docs/img/github-display-pat.png differ diff --git a/docs/img/github-generate-pat.png b/docs/img/github-generate-pat.png new file mode 100644 index 000000000..bdb3f2619 Binary files /dev/null and b/docs/img/github-generate-pat.png differ diff --git a/docs/img/github-pat-gist-scope.png b/docs/img/github-pat-gist-scope.png new file mode 100644 index 000000000..b008b691b Binary files /dev/null and b/docs/img/github-pat-gist-scope.png differ diff --git a/docs/img/github-pat-note.png b/docs/img/github-pat-note.png new file mode 100644 index 000000000..e02fff58f Binary files /dev/null and b/docs/img/github-pat-note.png differ diff --git a/docs/img/github-pat-repo-scope.png b/docs/img/github-pat-repo-scope.png new file mode 100644 index 000000000..34831d54c Binary files /dev/null and b/docs/img/github-pat-repo-scope.png differ diff --git a/docs/img/github-pat-workflow-scope.png b/docs/img/github-pat-workflow-scope.png new file mode 100644 index 000000000..d114f28d4 Binary files /dev/null and b/docs/img/github-pat-workflow-scope.png differ diff --git a/docs/img/windows-cli-save-pat.png b/docs/img/windows-cli-save-pat.png new file mode 100644 index 000000000..096db2eb6 Binary files /dev/null and b/docs/img/windows-cli-save-pat.png differ diff --git a/docs/img/windows-gui-add-pat.png b/docs/img/windows-gui-add-pat.png new file mode 100644 index 000000000..33eee603e Binary files /dev/null and b/docs/img/windows-gui-add-pat.png differ diff --git a/docs/img/windows-gui-credentials.png b/docs/img/windows-gui-credentials.png new file mode 100644 index 000000000..32c53d8f3 Binary files /dev/null and b/docs/img/windows-gui-credentials.png differ diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 000000000..4a023315e --- /dev/null +++ b/docs/install.md @@ -0,0 +1,240 @@ +# Install instructions + +There are multiple ways to install GCM on macOS, Windows, and Linux. Preferred +installation methods for each OS are designated with a :star:. + +## macOS + +### Homebrew :star: + +**Note:** If you have an existing installation of the 'Java GCM' on macOS and +you have installed this using Homebrew, this installation will be unlinked +(`brew unlink git-credential-manager`) when GCM is installed. + +#### Install + +```shell +brew tap microsoft/git +brew install --cask git-credential-manager-core +``` + +After installing you can stay up-to-date with new releases by running: + +```shell +brew upgrade git-credential-manager-core +``` + +#### Uninstall + +To uninstall, run the following: + +```shell +brew uninstall --cask git-credential-manager-core +``` + +--- + +### macOS Package + +#### Install + +Download and double-click the [installation package][latest-release] and follow +the instructions presented. + +#### Uninstall + +To uninstall, run the following: + +```shell +sudo /usr/local/share/gcm-core/uninstall.sh +``` + +--- + + + + + +## Linux + +**Note:** all Linux distributions +[require additional configuration][gcm-credstores] to use GCM. + +--- + +### .NET tool :star: + +See the [.NET tool](#.NET-tool) section below for instructions on this +installation method. + +--- + +### Debian package + +#### Install + +Download the latest [.deb package][latest-release], and run the following: + +```shell +sudo dpkg -i +git-credential-manager configure +``` + +#### Uninstall + +```shell +git-credential-manager unconfigure +sudo dpkg -r gcmcore +``` + +--- + +### Tarball + +#### Install + +Download the latest [tarball][latest-release], and run the following: + +```shell +tar -xvf -C /usr/local/bin +git-credential-manager configure +``` + +#### Uninstall + +```shell +git-credential-manager unconfigure +rm $(command -v git-credential-manager) +``` + +--- + +### Install from source helper script + +#### Install + +Ensure `curl` is installed: + +```shell +curl --version +``` + +If `curl` is not installed, please use your distribution's package manager +to install it. + +Download and run the script: + +```shell +curl -LO https://aka.ms/gcm/linux-install-source.sh && +sh ./linux-install-source.sh && +git-credential-manager-core configure +``` + +**Note:** You will be prompted to enter your credentials so that the script +can download GCM's dependencies using your distribution's package +manager. + +#### Uninstall + +[Follow these instructions][linux-uninstall] for your distribution. + +--- + +## Windows + +### Git for Windows :star: + +GCM is included with [Git for Windows][git-for-windows]. During installation +you will be asked to select a credential helper, with GCM listed as the default. + +![image][git-for-windows-screenshot] + +--- + +### Standalone installation + +You can also download the [latest installer][latest-release] for Windows to +install GCM standalone. + +**:warning: Important :warning:** + +Installing GCM as a standalone package on Windows will forcibly override the +version of GCM that is bundled with Git for Windows, **even if the version +bundled with Git for Windows is a later version**. + +There are two flavors of standalone installation on Windows: + +- User (`gcmuser-win*`): + + Does not require administrator rights. Will install only for the current user + and updates only the current user's Git configuration. + +- System (`gcm-win*`): + + Requires administrator rights. Will install for all users on the system and + update the system-wide Git configuration. + +To install, double-click the desired installation package and follow the +instructions presented. + +### Uninstall (Windows 10) + +To uninstall, open the Settings app and navigate to the Apps section. Select +"Git Credential Manager" and click "Uninstall". + +### Uninstall (Windows 7-8.1) + +To uninstall, open Control Panel and navigate to the Programs and Features +screen. Select "Git Credential Manager" and click "Remove". + +### Windows Subsystem for Linux (WSL) + +Git Credential Manager can be used with the [Windows Subsystem for Linux +(WSL)][ms-wsl] to enable secure authentication of your remote Git +repositories from inside of WSL. + +[Please see the GCM on WSL docs][gcm-wsl] for more information. + +--- + +## .NET tool + +GCM is available to install as a cross-platform [.NET +tool][dotnet-tool]. This is +the preferred install method for Linux because you can use it to install on any +[.NET-supported +distribution][dotnet-supported-distributions]. You +can also use this method on macOS or Windows if you so choose. + +**Note:** Make sure you have installed .NET before attempting to run the +following `dotnet tool` commands. + +#### Install + +```shell +dotnet tool install -g git-credential-manager +git-credential-manager configure +``` + +#### Update + +```shell +dotnet tool update -g git-credential-manager +``` + +#### Uninstall + +```shell +git-credential-manager unconfigure +dotnet tool uninstall -g git-credential-manager +``` + +[dotnet-supported-distributions]: https://learn.microsoft.com/en-us/dotnet/core/install/linux +[dotnet-tool]: https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools +[gcm-credstores]: credstores.md +[gcm-wsl]: wsl.md +[git-for-windows]: https://gitforwindows.org/ +[git-for-windows-screenshot]: https://user-images.githubusercontent.com/5658207/140082529-1ac133c1-0922-4a24-af03-067e27b3988b.png +[latest-release]: https://github.com/GitCredentialManager/git-credential-manager/releases/latest +[linux-uninstall]: linux-fromsrc-uninstall.md +[ms-wsl]: https://aka.ms/wsl# diff --git a/docs/linux-fromsrc-uninstall.md b/docs/linux-fromsrc-uninstall.md index 878528804..b58801c54 100644 --- a/docs/linux-fromsrc-uninstall.md +++ b/docs/linux-fromsrc-uninstall.md @@ -1,20 +1,32 @@ # Uninstalling after installing from source -These instructions will guide you in removing GCM after running the [install from source script](../src/linux/Packaging.Linux/install-from-source.sh) on your Linux distribution. +These instructions will guide you in removing GCM after running the +[install from source script][install-from-source] on your Linux distribution. :rotating_light: PROCEED WITH CAUTION :rotating_light: -For completeness, we provide uninstall instructions for _the GCM application, the GCM repo, and the maximum number of dependencies*_ for all distributions. This repo and these dependencies may or may not have already been present on your system when you ran the install from source script, and uninstalling them could impact other programs and/or your normal workflows. Please keep this in mind when following the instructions below. +For completeness, we provide uninstall instructions for _the GCM application, +the GCM repo, and the maximum number of dependencies*_ for all distributions. +This repo and these dependencies may or may not have already been present on +your system when you ran the install from source script, and uninstalling them +could impact other programs and/or your normal workflows. Please keep this in +mind when following the instructions below. -*Certain distributions require some dependencies of the script to function as expected, so we only include instructions to remove the non-required dependencies. +*Certain distributions require some dependencies of the script to function as +expected, so we only include instructions to remove the non-required +dependencies. ## All distributions -**Note:** If you ran the install from source script from a pre-existing clone of the `git-credential-manager` repo or outside of your `$HOME` directory, you will need to modify the final two commands below to point to the location of your pre-existing clone or the directory from which you ran the install from source script. +**Note:** If you ran the install from source script from a pre-existing clone of +the `git-credential-manager` repo or outside of your `$HOME` directory, you will +need to modify the final two commands below to point to the location of your +pre-existing clone or the directory from which you ran the install from source +script. ```console -git-credential-manager-core unconfigure && -sudo rm $(command -v git-credential-manager-core) && +git-credential-manager unconfigure && +sudo rm $(command -v git-credential-manager) && sudo rm -rf /usr/local/share/gcm-core && sudo rm -rf ~/git-credential-manager && sudo rm ~/install-from-source.sh @@ -22,7 +34,10 @@ sudo rm ~/install-from-source.sh ## Debian/Ubuntu -**Note:** If you had a pre-existing installation of dotnet that was not installed via `apt` or `apt-get` when you ran the install from source script, you will need to remove it using [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net) and remove `dotnet-*` from the below command. +**Note:** If you had a pre-existing installation of dotnet that was not +installed via `apt` or `apt-get` when you ran the install from source script, +you will need to remove it using [these instructions][uninstall-dotnet] and +remove `dotnet-*` from the below command. ```console sudo apt remove dotnet-* dpkg-dev apt-transport-https git curl wget @@ -30,7 +45,11 @@ sudo apt remove dotnet-* dpkg-dev apt-transport-https git curl wget ## Linux Mint -**Note:** If you had a pre-existing installation of dotnet when you ran the install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed and keep other versions, you can do so with [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net). +**Note:** If you had a pre-existing installation of dotnet when you ran the +install from source script that was not located at `~/.dotnet`, you will need to +modify the first command below to point to the custom install location. If you +would like to remove the specific version of dotnet that the script installed +and keep other versions, you can do so with [these instructions][uninstall-dotnet]. ```console sudo rm -rf ~/.dotnet && @@ -39,7 +58,11 @@ sudo apt remove git curl ## Fedora/CentOS/RHEL -**Note:** If you had a pre-existing installation of dotnet when you ran the install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed and keep other versions, you can do so with [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net). +**Note:** If you had a pre-existing installation of dotnet when you ran the +install from source script that was not located at `~/.dotnet`, you will need to +modify the first command below to point to the custom install location. If you +would like to remove the specific version of dotnet that the script installed +and keep other versions, you can do so with [these instructions][uninstall-dotnet]. ```console sudo rm -rf ~/.dotnet @@ -47,9 +70,17 @@ sudo rm -rf ~/.dotnet ## Alpine -**Note:** If you had a pre-existing installation of dotnet when you ran the install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed and keep other versions, you can do so with [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net). +**Note:** If you had a pre-existing installation of dotnet when you ran the +install from source script that was not located at `~/.dotnet`, you will need to +modify the first command below to point to the custom install location. If you +would like to remove the specific version of dotnet that the script installed +and keep other versions, you can do so with [these instructions][uninstall-dotnet]. ```console sudo rm -rf ~/.dotnet && -sudo apk del icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib which bash coreutils gcompat git curl +sudo apk del icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib which +bash coreutils gcompat git curl ``` + +[install-from-source]: ../src/linux/Packaging.Linux/install-from-source.sh +[uninstall-dotnet]: https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net diff --git a/docs/migration.md b/docs/migration.md index 88ba8da89..8ad6e392e 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -4,11 +4,17 @@ ### GCM_AUTHORITY -This setting (and the corresponding `credential.authority` configuration) is deprecated and should be replaced with the `GCM_PROVIDER` (or corresponding `credential.authority` configuration) setting. +This setting (and the corresponding `credential.authority` configuration) is +deprecated and should be replaced with the `GCM_PROVIDER` (or corresponding +`credential.authority` configuration) setting. -Because both Basic HTTP authentication and Windows Integrated Authentication (WIA) are now handled by one provider, if you specified `basic` as your authority you also need to disable WIA using `GCM_ALLOW_WINDOWSAUTH` / `credential.allowWindowsAuth`. +Because both Basic HTTP authentication and Windows Integrated Authentication +(WIA) are now handled by one provider, if you specified `basic` as your +authority you also need to disable WIA using `GCM_ALLOW_WINDOWSAUTH` / +`credential.allowWindowsAuth`. -The following table shows the correct replacement for all legacy authorities values: +The following table shows the correct replacement for all legacy authorities +values: GCM_AUTHORITY (credential.authority)|→|GCM_PROVIDER (credential.provider)|GCM_ALLOW_WINDOWSAUTH (credential.allowWindowsAuth) -|-|-|- @@ -17,7 +23,8 @@ GCM_AUTHORITY (credential.authority)|→|GCM_PROVIDER (credential.provider)|G `basic`|→|`generic`|`false` `integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|→|`generic`|`true` _(default)_ -For example if you had previous set the authority for the `example.com` host to `basic`.. +For example if you had previous set the authority for the `example.com` host to +`basic`.. ```shell git config --global credential.example.com.authority basic diff --git a/docs/multiple-users.md b/docs/multiple-users.md index 7446aef0e..b02b35a42 100644 --- a/docs/multiple-users.md +++ b/docs/multiple-users.md @@ -1,24 +1,47 @@ # Multiple users -If you work with multiple different identities on a single Git hosting service, you may be wondering if Git Credential Manager (GCM) supports this workflow. The answer is yes, with a bit of complexity due to how it interoperates with Git. +If you work with multiple different identities on a single Git hosting service, +you may be wondering if Git Credential Manager (GCM) supports this workflow. The +answer is yes, with a bit of complexity due to how it interoperates with Git. ## Foundations: Git and Git hosts -Git itself doesn't have a single, strong concept of "user". There's the `user.name` and `user.email` which get embedded into commit headers/trailers, but these are arbitrary strings. GCM doesn't interact with this notion of a user at all. You can put whatever you want into your `user.*` config, and nothing in GCM will change at all. +Git itself doesn't have a single, strong concept of "user". There's the +`user.name` and `user.email` which get embedded into commit headers/trailers, +but these are arbitrary strings. GCM doesn't interact with this notion of a user +at all. You can put whatever you want into your `user.*` config, and nothing in +GCM will change at all. -Separate from the user strings in commits, Git recognizes the "user" part of a remote URL or a credential. These are not often used, at least by default, in the web UI of major Git hosts. +Separate from the user strings in commits, Git recognizes the "user" part of a +remote URL or a credential. These are not often used, at least by default, in +the web UI of major Git hosts. -Git hosting providers (like GitHub or Bitbucket) _do_ have a concept of "user". Typically it's an identity like a username or email address, plus a password or other credential to perform actions as that user. You may have guessed by now that GCM (the Git **Credential** Manager) does work with this notion of a user. +Git hosting providers (like GitHub or Bitbucket) _do_ have a concept of "user". +Typically it's an identity like a username or email address, plus a password or +other credential to perform actions as that user. You may have guessed by now +that GCM (the Git **Credential** Manager) does work with this notion of a user. ## People, identities, credentials, oh my -You (a physical person) may have one or more user accounts (identities) with one or more Git hosting providers. Since most Git hosts don't put a "user" part in their URLs, by default, Git will treat the user part for a remote as the empty string. If you have multiple identities on one domain, you'll need to insert a unique user part per-identity yourself. - -There are good reasons for having multiple identities on one domain. You might use one GitHub identity for your personal work, another for your open source work, and a third for your employer's work. You can ask Git to assign a different credential to different repositories hosted on the same provider. HTTPS URLs include an optional "name" part before an `@` sign in the domain name, and you can use this to force Git to distinguish multiple users. This should likely be your username on the Git hosting service, since there are cases where GCM will use it like a username. +You (a physical person) may have one or more user accounts (identities) with one +or more Git hosting providers. Since most Git hosts don't put a "user" part in +their URLs, by default, Git will treat the user part for a remote as the empty +string. If you have multiple identities on one domain, you'll need to insert a +unique user part per-identity yourself. + +There are good reasons for having multiple identities on one domain. You might +use one GitHub identity for your personal work, another for your open source +work, and a third for your employer's work. You can ask Git to assign a +different credential to different repositories hosted on the same provider. +HTTPS URLs include an optional "name" part before an `@` sign in the domain +name, and you can use this to force Git to distinguish multiple users. This +should likely be your username on the Git hosting service, since there are +cases where GCM will use it like a username. ## Setting it up -As an example, let's say you're working on multiple repositories hosted at the same domain name. +As an example, let's say you're working on multiple repositories hosted at the +same domain name. | Repo URL | Identity | |----------|----------| @@ -26,7 +49,9 @@ As an example, let's say you're working on multiple repositories hosted at the s | `https://example.com/more-open-source/app.git` | `contrib123` | | `https://example.com/big-company/secret-repo.git` | `employee9999` | -When you clone these repos, include the identity and an `@` before the domain name in order to force Git and GCM to use different identities. If you've already cloned the repos, you can update the remote URL to include the identity. +When you clone these repos, include the identity and an `@` before the domain +name in order to force Git and GCM to use different identities. If you've +already cloned the repos, you can update the remote URL to include the identity. ### Example: fresh clones @@ -50,4 +75,7 @@ git remote set-url origin https://employee9999@example.com/big-company/secret-re ## Azure DevOps -[Azure DevOps has some additional, optional complexity](azrepos-users-and-tokens.md) which you should also be aware of if you're using it. +[Azure DevOps has some additional, optional complexity][azure-access-tokens] +which you should also be aware of if you're using it. + +[azure-access-tokens]: azrepos-users-and-tokens.md diff --git a/docs/netconfig.md b/docs/netconfig.md index 35cf9dc80..cf312336f 100644 --- a/docs/netconfig.md +++ b/docs/netconfig.md @@ -1,12 +1,17 @@ # Network and HTTP configuration -Git Credential Manager's network and HTTP(S) behavior can be configured in a few different ways via [environment variables](environment.md) and [configuration options](configuration.md). +Git Credential Manager's network and HTTP(S) behavior can be configured in a few +different ways via [environment variables][environment] and +[configuration options][configuration]. ## HTTP Proxy -If your computer sits behind a network firewall that requires the use of a proxy server to reach repository remotes or the wider Internet, there are various methods for configuring GCM to use a proxy. +If your computer sits behind a network firewall that requires the use of a +proxy server to reach repository remotes or the wider Internet, there are +various methods for configuring GCM to use a proxy. -The simplest way to configure a proxy for _all_ HTTP(S) remotes is to [use the standard Git HTTP(S) proxy setting `http.proxy`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy). +The simplest way to configure a proxy for _all_ HTTP(S) remotes is to +[use the standard Git HTTP(S) proxy setting `http.proxy`][git-http-proxy]. For example to configure a proxy for all remotes for the current user: @@ -14,31 +19,43 @@ For example to configure a proxy for all remotes for the current user: git config --global http.proxy http://proxy.example.com ``` -To specify a proxy for a particular remote you can [use the `remote..proxy` repository-level setting](https://git-scm.com/docs/git-config#Documentation/git-config.txt-remoteltnamegtproxy), for example: +To specify a proxy for a particular remote you can +[use the `remote..proxy` repository-level setting][git-remote-name-proxy], +for example: ```shell git config --local remote.origin.proxy http://proxy.example.com ``` -The advantage to using these standard configuration options is that in addition to GCM being configured to use the proxy, Git itself will be configured at the same time. This is probably the most commonly desired case in environments behind an Internet-blocking firewall. +The advantage to using these standard configuration options is that in addition +to GCM being configured to use the proxy, Git itself will be configured at the +same time. This is probably the most commonly desired case in environments +behind an Internet-blocking firewall. ### Authenticated proxies -Some proxy servers do not accept anonymous connections and require authentication. In order to specify the credentials to be used with a proxy, you can specify the username and password as part of the proxy URL setting. +Some proxy servers do not accept anonymous connections and require +authentication. In order to specify the credentials to be used with a proxy, +you can specify the username and password as part of the proxy URL setting. -The format follows [RFC 3986 section 3.2.1](https://tools.ietf.org/html/rfc3986#section-3.2.1) by including the credentials in the 'user information' part of the URI. The password is optional. +The format follows [RFC 3986 section 3.2.1][rfc-3986-321] by including the +credentials in the 'user information' part of the URI. The password is optional. ```text protocol://username[:password]@hostname ``` -For example, to specify the username `john.doe` and the password `letmein123` for the proxy server `proxy.example.com`: +For example, to specify the username `john.doe` and the password `letmein123` +for the proxy server `proxy.example.com`: ```text https://john.doe:letmein123@proxy.example.com ``` -If you have special characters (as defined by [RFC 3986 section 2.2](https://tools.ietf.org/html/rfc3986#section-2.2)) in your username or password such as `:`, `@`, or any other non-URL friendly character you can URL-encode them ([section 2.1](https://tools.ietf.org/html/rfc3986#section-2.2)). +If you have special characters (as defined by +[RFC 3986 section 2.2][rfc-3986-22]) in your username or password such as `:`, +`@`, or any other non-URL friendly character you can URL-encode them +([section 2.1][rfc-3986-21]). For example, a space character would be encoded with `%20`. @@ -53,7 +70,8 @@ GCM supports other ways of configuring a proxy for convenience and compatibility - `http_proxy` - `https_proxy`/`HTTPS_PROXY` - `all_proxy`/`ALL_PROXY` -1. `GCM_HTTP_PROXY` environment variable (_**only** respected by GCM; **deprecated**_) +1. `GCM_HTTP_PROXY` environment variable (_**only** respected by GCM; +**deprecated**_) Note that with the cURL environment variables there are both lowercase and uppercase variants. @@ -64,8 +82,7 @@ consistent with how libcurl (and therefore Git) operates. The `http_proxy` variable exists only in the lowercase variant and libcurl does _not_ consider any uppercase form. _GCM also reflects this behavior._ -See -for more information. +See [the curl docs][curl-proxy-env-vars] for more information. ### Bypassing addresses @@ -73,13 +90,13 @@ In some circumstances you may wish to bypass a configured proxy for specific addresses. GCM supports the cURL environment variable `no_proxy` (and `NO_PROXY`) for this scenario, as does Git itself. -Like with the [other cURL proxy environment variables](#other-proxy-options), +Like with the [other cURL proxy environment variables][other-proxy-options], the lowercase variant will take precedence over the uppercase form. This environment variable should contain a comma-separated or space-separated list of host names that should not be proxied (should connect directly). -GCM attempts to match [libcurl's behaviour](https://curl.se/libcurl/c/CURLOPT_NOPROXY.html), +GCM attempts to match [libcurl's behaviour][curlopt-noproxy], which is briefly summarized here: - a value of `*` disables proxying for all hosts; @@ -107,37 +124,54 @@ no_proxy="contoso.com,www.fabrikam.com" ## TLS Verification -If you are using self-signed TLS (SSL) certificates with a self-hosted host provider such as GitHub Enterprise Server or Azure DevOps Server (previously TFS), you may see the following error message when attempting to connect using Git and/or GCM: +If you are using self-signed TLS (SSL) certificates with a self-hosted host +provider such as GitHub Enterprise Server or Azure DevOps Server (previously +TFS), you may see the following error message when attempting to connect using +Git and/or GCM: ```shell $ git clone https://ghe.example.com/john.doe/myrepo fatal: The remote certificate is invalid according to the validation procedure. ``` -The **recommended and safest option** is to acquire a TLS certificate signed by a public trusted certificate authority (CA). There are multiple public CAs; here is a non-exhaustive list to consider: [Let's Encrypt](https://letsencrypt.org/), [Comodo](https://www.comodoca.com/), [Digicert](https://www.digicert.com/), [GoDaddy](https://www.godaddy.com/web-security/ssl-certificate), [GlobalSign](https://www.globalsign.com/en/ssl/). +The **recommended and safest option** is to acquire a TLS certificate signed by +a public trusted certificate authority (CA). There are multiple public CAs; here +is a non-exhaustive list to consider: [Let's Encrypt][lets-encrypt], +[Comodo][comodo], [Digicert][digicert], [GoDaddy][godaddy], +[GlobalSign][globalsign]. -If it is not possible to **obtain a TLS certificate from a trusted 3rd party** then you should try to add the _specific_ self-signed certificate or one of the CA certificates in the verification chain to your operating system's trusted certificate store ([macOS](https://support.apple.com/en-gb/guide/keychain-access/kyca2431/mac), [Windows](https://blogs.technet.microsoft.com/sbs/2008/05/08/installing-a-self-signed-certificate-as-a-trusted-root-ca-in-windows-vista/)). +If it is not possible to **obtain a TLS certificate from a trusted 3rd party** +then you should try to add the _specific_ self-signed certificate or one of the +CA certificates in the verification chain to your operating system's trusted +certificate store ([macOS][mac-keychain-access], [Windows][install-cert-vista]). -If you are _unable_ to either **obtain a trusted certificate**, or trust the self-signed certificate you can disable certificate verification in Git and GCM. +If you are _unable_ to either **obtain a trusted certificate**, or trust the +self-signed certificate you can disable certificate verification in Git and GCM. --- **Security Warning** :warning: -Disabling verification of TLS (SSL) certificates removes protection against a [man-in-the-middle (MITM) attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). +Disabling verification of TLS (SSL) certificates removes protection against a +[man-in-the-middle (MITM) attack][mitm-attack]. -Only disable certificate verification if you are sure you need to, are aware of all the risks, and are unable to trust specific self-signed certificates (as described above). +Only disable certificate verification if you are sure you need to, are aware of +all the risks, and are unable to trust specific self-signed certificates +(as described above). --- -The [environment variable `GIT_SSL_NO_VERIFY`](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_networking) and [Git configuration option `http.sslVerify`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify) can be used to control TLS (SSL) certificate verification. +The [environment variable `GIT_SSL_NO_VERIFY`][git-ssl-no-verify] and +[Git configuration option `http.sslVerify`][git-http-ssl-verify] can be used to +control TLS (SSL) certificate verification. -To disable verification for a specific remote (for example ): +To disable verification for a specific remote (for example `https://example.com`): ```shell git config --global http.https://example.com.sslVerify false ``` -To disable verification for the current user for **_all remotes_** (**not recommended**): +To disable verification for the current user for **_all remotes_** (**not +recommended**): ```shell # Environment variable (Windows) @@ -152,4 +186,29 @@ git config --global http.sslVerify false --- -**Note:** You may also experience similar verification errors if you are using a network traffic inspection tool such as [Telerik Fiddler](https://www.telerik.com/fiddler). If you are using such tools please consult their documentation for trusting the proxy root certificates. +**Note:** You may also experience similar verification errors if you are using a +network traffic inspection tool such as [Telerik Fiddler][telerik-fiddler]. If +you are using such tools please consult their documentation for trusting the +proxy root certificates. + +[environment]: environment.md +[configuration]: configuration.md +[git-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy +[git-remote-name-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-remoteltnamegtproxy +[rfc-3986-321]: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1 +[rfc-3986-22]: https://www.rfc-editor.org/rfc/rfc3986#section-2.2 +[rfc-3986-21]: https://www.rfc-editor.org/rfc/rfc3986#section-2.1 +[curl-proxy-env-vars]: https://everything.curl.dev/usingcurl/proxies#proxy-environment-variables +[other-proxy-options]: #other-proxy-options +[curlopt-noproxy]: https://curl.se/libcurl/c/CURLOPT_NOPROXY.html +[lets-encrypt]: https://letsencrypt.org/ +[comodo]: https://www.comodoca.com/ +[digicert]: https://www.digicert.com/ +[godaddy]: https://www.godaddy.com/ +[globalsign]: https://www.globalsign.com +[mac-keychain-access]: https://support.apple.com/en-gb/guide/keychain-access/kyca2431/mac +[install-cert-vista]: https://blogs.technet.microsoft.com/sbs/2008/05/08/installing-a-self-signed-certificate-as-a-trusted-root-ca-in-windows-vista/ +[mitm-attack]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack +[git-ssl-no-verify]: https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_networking +[git-http-ssl-verify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify +[telerik-fiddler]: https://www.telerik.com/fiddler diff --git a/docs/rename.md b/docs/rename.md new file mode 100644 index 000000000..b35d164e7 --- /dev/null +++ b/docs/rename.md @@ -0,0 +1,166 @@ +# Git Credential Manager Rename + +In November 2021, _"Git Credential Manager Core"_ was [renamed][rename-pr] to +simply _"Git Credential Manager"_, dropping the "Core" moniker. We announced the +new name in a [GitHub blog post][rename-blog], along with the new home for the +project in it's own [organization][gcm-org]. + +![Git Credential Manager Core renamed](img/gcmcore-rename.png) + +At the time, the actual exectuable name was not updated and continued to be +`git-credential-manager-core`. As of [VERSION][rename-ver], the executable has +been renamed to `git-credential-manager`, matching the new project name. + +## Rename transition + +If you continue to use the `git-credential-manager-core` executable name you may +see warning messages like below: + +```console +warning: git-credential-manager-core was renamed to git-credential-manager +warning: see https://aka.ms/gcm/rename for more information +``` + +Since the executable was renamed in VERSION, GCM has also included symlinks +using the old name in order to ensure no one's setups would immediately break. + +These links will remain until _two_ major Git versions are released after GCM +VERSION, _**at which point the symlinks will no longer be included**_. + +It is recommended to update your Git configuration to use the new executable +name as soon as possible to prevent any issues in the future. + +## How to update + +### Git for Windows + +If you are using GCM bundled with Git for Windows (recommended), you should make +sure you have updated to at least version WINGIT_VERSION. + +[Download the latest Git for Windows ⬇️][git-windows] + +### Windows standalone installer + +If you are using GCM installed either by the user (`gcmuser-*.exe`) or system +(`gcm-*.exe`) installers on Windows, you should uninstall the current version +first and then download and install the [latest version][gcm-latest]. + +Uninstall instructions for your Windows version can be found +[here][win-standalone-instr]. + +### macOS Homebrew + +> **Note:** As of October 2022 the old `git-credential-manager-core` cask name +> is still used. In the future we plan to rename the package to drop the `-core` +> suffix. + +If you use Homebrew to install GCM on macOS you should use `brew upgrade` to +install the latest version. + +```sh +brew upgrade git-credential-manager-core +``` + +### macOS package + +If you use the .pkg file to install GCM on macOS, you should first uninstall the +current version, and then install the [latest package][gcm-latest]. + +```sh +sudo /usr/local/share/gcm-core/uninstall.sh +installer -pkg -target / +``` + +### Linux Debian package + +If you use the .deb Debian package to install GCM on Linux, you should first +`unconfigure` the current version, uninstall the package, and then install and +`configure` the [latest version][gcm-latest]. + +```sh +git-credential-manager-core unconfigure +sudo dpkg -r gcmcore +sudo dpkg -i +git-credential-manager configure +``` + +### Linux tarball + +If you are using the pre-built GCM binaries on Linux from our tarball, you +should first `unconfigure` the current version before extracting the [latest +binaries][gcm-latest]. + +```sh +git-credential-manager-core unconfigure +rm $(command -v git-credential-manager-core) +tar -xvf -C /usr/local/bin +git-credential-manager configure +``` + +### Troubleshooting + +If after updating your GCM installations if you are still seeing the +[warning][warnings] messages you can try manually editing your Git configuration +to point to the correct GCM executable name. + +Start by listing all Git configuration for `credential.helper`, including which +files the particular config entries are located in, using the following command: + +```sh +git config --show-origin --get-all credential.helper +``` + +On Mac or Linux you should see something like this: + + +```shell-session +$ git config --show-origin --get-all credential.helper +file:/opt/homebrew/etc/gitconfig credential.helper=osxkeychain +file:/Users/jdoe/.gitconfig credential.helper= +file:/Users/jdoe/.gitconfig credential.helper=/usr/local/share/gcm-core/git-credential-manager-core +``` + +On Windows you should see something like this: + +```shell-session +> git config --show-origin --get-all credential.helper +file:C:/Program Files/Git/etc/gitconfig credential.helper=manager-core +``` + + +Look out for entries that include `git-credential-manager-core` or +`manager-core`; these should be replaced and updated to `git-credential-manager` +or `manager` respectively. + +> **Note:** When updating the Git configuration file in your home directory +> (`$HOME/.gitconfig` or `%USERPROFILE\.gitconfig%`) you should ensure there are +> is an additional blank entry for `credential.helper` before the GCM entry. +> +> **Mac/Linux** +> +> ```ini +> [credential] +> helper = +> helper = /usr/local/share/gcm-core/git-credential-manager +> ``` +> +> **Windows** +> +> ```ini +> [credential] +> helper = +> helper = C:/Program\\ Files\\ \\(x86\\)/Git\\ Credential\\ Manager/git-credential-manager.exe +> ``` +> +> The blank entry is important as it makes sure GCM is the only credential +> helper that is configured, and overrides any helpers configured at the system/ +> machine-wide level. + +[rename-pr]: https://github.com/GitCredentialManager/git-credential-manager/pull/541 +[rename-blog]: https://github.blog/2022-04-07-git-credential-manager-authentication-for-everyone/#universal-git-authentication +[gcm-org]: https://github.com/GitCredentialManager +[rename-ver]: https://github.com/GitCredentialManager/git-credential-manager/releases +[git-windows]: https://git-scm.com/download/win +[gcm-latest]: https://aka.ms/gcm/latest +[warnings]: #rename-transition +[win-standalone-instr]: ../README.md#standalone-installation diff --git a/docs/usage.md b/docs/usage.md index 1f64cbd1a..3e0f6d8c9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,12 +1,15 @@ # Command-line usage -After installation, Git will use Git Credential Manager and you will only need to interact with any authentication dialogs asking for credentials. -GCM stays invisible as much as possible, so ideally you’ll forget that you’re depending on GCM at all. +After installation, Git will use Git Credential Manager and you will only need +to interact with any authentication dialogs asking for credentials. +GCM stays invisible as much as possible, so ideally you’ll forget that you’re +depending on GCM at all. -Assuming GCM has been installed, use your favorite terminal to execute the following commands to interact directly with GCM. +Assuming GCM has been installed, use your favorite terminal to execute the +following commands to interact directly with GCM. ```shell -git credential-manager-core [ []] +git credential-manager [ []] ``` ## Commands @@ -23,7 +26,8 @@ Displays the current version. Commands for interaction with Git. You shouldn't need to run these manually. -Read the [Git manual](https://git-scm.com/docs/gitcredentials#_custom_helpers) about custom helpers for more information. +Read the [Git manual][git-credentials-custom-helpers] about custom helpers for +more information. ### configure/unconfigure @@ -33,7 +37,12 @@ Set your user-level Git configuration (`~/.gitconfig`) to use GCM. If you pass ### azure-repos -Interact with the Azure Repos host provider to bind/unbind user accounts to Azure DevOps -organizations or specific remote URLs, and manage the authentication authority cache. +Interact with the Azure Repos host provider to bind/unbind user accounts to +Azure DevOps organizations or specific remote URLs, and manage the +authentication authority cache. -For more information about managing user account bindings see [here](azrepos-users-and-tokens.md#useraccounts). +For more information about managing user account bindings see +[here][azure-access-tokens-ua]. + +[azure-access-tokens-ua]: azrepos-users-and-tokens.md#useraccounts +[git-credentials-custom-helpers]: https://git-scm.com/docs/gitcredentials#_custom_helpers diff --git a/docs/windows-broker.md b/docs/windows-broker.md index 62b422ee5..c767c172d 100644 --- a/docs/windows-broker.md +++ b/docs/windows-broker.md @@ -1,108 +1,158 @@ # Web Account Manager integration -Git Credential Manager (GCM) knows how to integrate with the [Web Account Manager (WAM)](https://docs.microsoft.com/azure/active-directory/devices/concept-primary-refresh-token#key-terminology-and-components) feature of Windows. -GCM uses WAM to store credentials for Azure DevOps. -Authentication requests are said to be "brokered" to the operating system. -Currently, GCM will share authentication state with a few other Microsoft developer tools like Visual Studio and the Azure CLI, meaning fewer authentication prompts. -Enabling WAM integration may also be required with certain [Conditional Access policies](https://docs.microsoft.com/azure/active-directory/conditional-access/overview), which enterprises use to help protect their assets, including source code. - -Integration with the WAM broker offers convenience and other benefits, but may also make unexpected other changes on your device. -On a device owned and managed by your institution or employer, WAM is probably the right choice. -On a personal device or a device owned by a different institution (e.g. if you're a contractor working for Company A with access to resources at Company B), there are surprising behaviors that you should be aware of before enabling WAM integration. - -Note that this only affects [Azure DevOps](https://dev.azure.com). +Git Credential Manager (GCM) knows how to integrate with the +[Web Account Manager (WAM)][azure-refresh-token-terms] feature of Windows. GCM +uses WAM to store credentials for Azure DevOps. Authentication requests are said +to be "brokered" to the operating system. Currently, GCM will share +authentication state with a few other Microsoft developer tools like Visual +Studio and the Azure CLI, meaning fewer authentication prompts. Enabling WAM +integration may also be required with certain +[Conditional Access policies][azure-conditional-access], which enterprises use +to help protect their assets, including source code. + +Integration with the WAM broker offers convenience and other benefits, but may +also make unexpected other changes on your device. On a device owned and managed +by your institution or employer, WAM is probably the right choice. On a personal +device or a device owned by a different institution (e.g. if you're a contractor +working for Company A with access to resources at Company B), there are +surprising behaviors that you should be aware of before enabling WAM integration. + +Note that this only affects [Azure DevOps][azure-devops]. It doesn't impact authentication with GitHub, Bitbucket, or any other Git host. ## How to enable -You can opt-in to WAM support by setting the environment variable [`GCM_MSAUTH_USEBROKER`](https://github.com/GitCredentialManager/git-credential-manager/blob/main/docs/environment.md#gcm_msauth_usebroker-experimental) or setting the Git configuration value [`credential.msauthUseBroker`](https://github.com/GitCredentialManager/git-credential-manager/blob/main/docs/configuration.md#credentialmsauthusebroker-experimental). +You can opt-in to WAM support by setting the environment variable +[`GCM_MSAUTH_USEBROKER`][GCM_MSAUTH_USEBROKER] or setting the Git configuration +value [`credential.msauthUseBroker`][credential.msauthUseBroker]. ## Features -When you turn on WAM support, GCM can cooperate with Windows and with other WAM-enabled software on your machine. -This means a more seamless experience, fewer multi-factor authentication prompts, and the ability to use additional authentication technologies like smart cards and Windows Hello. -These convenience and security features make a good case for enabling WAM. +When you turn on WAM support, GCM can cooperate with Windows and with other +WAM-enabled software on your machine. This means a more seamless experience, +fewer multi-factor authentication prompts, and the ability to use additional +authentication technologies like smart cards and Windows Hello. These +convenience and security features make a good case for enabling WAM. ## Surprising behaviors -The WAM and Windows identity systems are complex, addressing a very broad range of customer use cases. -What works for a solo home user may not be adequate for a corporate-managed fleet of 100,000 devices and vice versa. -The GCM team isn't responsible for the user experience or choices made by WAM, but by integrating with WAM, we inherit some of those choices. -Therefore, we want you to be aware of some defaults and experiences if you choose to use WAM integration. +The WAM and Windows identity systems are complex, addressing a very broad range +of customer use cases. What works for a solo home user may not be adequate for a +corporate-managed fleet of 100,000 devices and vice versa. The GCM team isn't +responsible for the user experience or choices made by WAM, but by integrating +with WAM, we inherit some of those choices. Therefore, we want you to be aware +of some defaults and experiences if you choose to use WAM integration. ### For work or school accounts (Azure AD-backed identities) -When you sign into an Azure DevOps organization backed by Azure AD (often your company or school email), if your machine is already joined to Azure AD matching that Azure DevOps organization, you'll get a seamless and easy-to-use experience. +When you sign into an Azure DevOps organization backed by Azure AD (often your +company or school email), if your machine is already joined to Azure AD matching +that Azure DevOps organization, you'll get a seamless and easy-to-use experience. -If your machine isn't Azure AD-joined, or is Azure AD-joined to a different tenant, WAM will present you with a dialog box suggesting you stay signed in and allow the organization to manage your device. -The dialog box has changed a bit in various versions of Windows; here are two examples from 2021: +If your machine isn't Azure AD-joined, or is Azure AD-joined to a different +tenant, WAM will present you with a dialog box suggesting you stay signed in and +allow the organization to manage your device. The dialog box has changed a bit +in various versions of Windows; here are two examples from 2021: -![Consent dialog pre-21H1](img/aad-questions.png) +![Consent dialog pre-21H1][aad-questions] -![Consent dialog post-21H1](img/aad-questions-21H1.png) +![Consent dialog post-21H1][aad-questions-21h1] Depending on what you click, one of three things can happen: -- If you leave "allow my organization to manage my device" checked and click "OK", your computer will be registered with the Azure AD tenant backing the organization. -It may also be MDM-enrolled ("Mobile Device Management" -- think Intune, AirWatch, MobileIron, etc.), meaning an administrator can deploy policies to your machine: requiring certain kinds of sign-in, turning on antivirus and firewall software, and enabling BitLocker. -Your identity will also be available to other apps on the computer for signing in, some of which may do so automatically. -![Example of policies pushed to an Intune-enrolled device](img/aad-bitlocker.png) -- If you uncheck "allow my organization to manage my device" and click "OK", your computer will be registered with Azure AD but will not be MDM-enrolled. +- If you leave "allow my organization to manage my device" checked and click +"OK", your computer will be registered with the Azure AD tenant backing the +organization. +It may also be MDM-enrolled ("Mobile Device Management" -- think Intune, +AirWatch, MobileIron, etc.), meaning an administrator can deploy policies to +your machine: requiring certain kinds of sign-in, turning on antivirus and +firewall software, and enabling BitLocker. +Your identity will also be available to other apps on the computer for signing +in, some of which may do so automatically. +![Example of policies pushed to an Intune-enrolled device][aad-bitlocker] +- If you uncheck "allow my organization to manage my device" and click "OK", +your computer will be registered with Azure AD but will not be MDM-enrolled. Your identity will be available to other apps on the computer for signing in. -Other apps may log you in automatically or prompt you again to allow your organization to manage your device. -Despite joining Azure AD, your organization's Conditional Access policies may still prevent you from accessing Azure DevOps. +Other apps may log you in automatically or prompt you again to allow your +organization to manage your device. Despite joining Azure AD, your +organization's Conditional Access policies may still prevent you from accessing +Azure DevOps. If so, you'll be prompted with instructions on how to enroll in MDM. -- If you instead click "No, sign in to this app only", your machine will not be joined to Azure AD or MDM-enrolled, so no policies can be enforced, and your identity won't be made available to other apps on the computer. -Similar to the above, your organization's Conditional Access policies may prevent you from proceeding. +- If you instead click "No, sign in to this app only", your machine will not be +joined to Azure AD or MDM-enrolled, so no policies can be enforced, and your +identity won't be made available to other apps on the computer. +Similar to the above, your organization's Conditional Access policies may +prevent you from proceeding. -If Conditional Access is required to access your organization's Git repositories, you can [enable WAM integration](environment.md#GCM_MSAUTH_USEBROKER-experimental) (or follow other instructions your organization provides). +If Conditional Access is required to access your organization's Git repositories, +you can [enable WAM integration][GCM_MSAUTH_USEBROKER] (or follow other +instructions your organization provides). #### Removing device management -If you've allowed your computer to be managed and want to undo it, you can go into **Settings**, **Accounts**, **Access work or school**. -In the section where you see your email address and organization name, click **Disconnect**. +If you've allowed your computer to be managed and want to undo it, you can go +into **Settings**, **Accounts**, **Access work or school**. +In the section where you see your email address and organization name, click +**Disconnect**. -![Finding your work or school account](img/aad-work-school.png) +![Finding your work or school account][aad-work-school] -![Disconnecting from Azure AD](img/aad-disconnect.png) +![Disconnecting from Azure AD][aad-disconnect] ### For Microsoft accounts -When you sign into an Azure DevOps organization backed by Microsoft account (MSA) identities (email addresses like `@outlook.com` or `@gmail.com` fall into this category), you may be prompted to select an existing "work or school account" or use a different one. +When you sign into an Azure DevOps organization backed by Microsoft account +(MSA) identities (email addresses like `@outlook.com` or `@gmail.com` fall into +this category), you may be prompted to select an existing "work or school +account" or use a different one. -In order to sign in with an MSA you should continue and select "Use a different [work or school] account", but enter your MSA credentials when prompted. -This is due to a configuration outside of our control. -We expect this experience to improve over time and a "personal account" option to be presented in the future. +In order to sign in with an MSA you should continue and select "Use a different +[work or school] account", but enter your MSA credentials when prompted. This is +due to a configuration outside of our control. We expect this experience to +improve over time and a "personal account" option to be presented in the future. -![Initial dialog to choose an existing or different account](img/get-signed-in.png) +![Initial dialog to choose an existing or different account][ms-sign-in] -If you've connected your MSA to Windows or signed-in to other Microsoft applications such as Office, then you may see this account listed in the authentication prompts when using GCM. -For any connected MSA, you can control whether or not the account is available to other Microsoft applications in **Settings**, **Accounts**, **Emails & accounts**: +If you've connected your MSA to Windows or signed-in to other Microsoft +applications such as Office, then you may see this account listed in the +authentication prompts when using GCM. For any connected MSA, you can control +whether or not the account is available to other Microsoft applications in +**Settings**, **Accounts**, **Emails & accounts**: -![Allow all Microsoft apps to access your identity](img/all-microsoft.png) +![Allow all Microsoft apps to access your identity][all-ms-apps] -![Microsoft apps must ask to access your identity](img/apps-must-ask.png) +![Microsoft apps must ask to access your identity][apps-must-ask] Two very important things to note: -- If you haven't connected any Microsoft accounts to Windows before, the first account you connect will cause the local Windows user account to be converted to a connected account. -- In addition, you can't change the usage preference for the first Microsoft account connected to Windows: all Microsoft apps will be able to sign you in with that account. +- If you haven't connected any Microsoft accounts to Windows before, the first +account you connect will cause the local Windows user account to be converted to +a connected account. +- In addition, you can't change the usage preference for the first Microsoft +account connected to Windows: all Microsoft apps will be able to sign you in +with that account. -As far as we can tell, there are no workarounds for either of these behaviors (other than to not use the WAM broker). +As far as we can tell, there are no workarounds for either of these behaviors +(other than to not use the WAM broker). ## Running as administrator -The Windows broker ("WAM") makes heavy use of [COM](https://docs.microsoft.com/en-us/windows/win32/com/the-component-object-model), a remote procedure call (RPC) technology built into Windows. -In order to integrate with WAM, Git Credential Manager and the underlying [Microsoft Authentication Library (MSAL)](https://aka.ms/msal-net) must use COM interfaces and RPCs. -When you run Git Credential Manager as an elevated process, some of the calls made between GCM and WAM may fail due to differing process security levels. -This can happen when you run `git` from an Administrator command-prompt or perform Git operations from Visual Studio running as Administrator. +The Windows broker ("WAM") makes heavy use of [COM][ms-com], a remote procedure +call (RPC) technology built into Windows. In order to integrate with WAM, Git +Credential Manager and the underlying +[Microsoft Authentication Library (MSAL)][msal-dotnet] must use COM interfaces +and RPCs. When you run Git Credential Manager as an elevated process, some of +the calls made between GCM and WAM may fail due to differing process security +levels. This can happen when you run `git` from an Administrator command-prompt +or perform Git operations from Visual Studio running as Administrator. If you've enabled using the broker, GCM will check whether it's running in an -elevated process. -If it is, GCM will automatically attempt to modify the COM security settings for the running process so that GCM and WAM can work together. +elevated process. If it is, GCM will automatically attempt to modify the COM +security settings for the running process so that GCM and WAM can work together. However, this automatic process security change is not guaranteed to succeed. -Various external factors like registry or system-wide COM settings may cause it to fail. -If GCM can't modify the process's COM security settings, GCM prints a warning message and won't be able to use the broker. +Various external factors like registry or system-wide COM settings may cause it +to fail. If GCM can't modify the process's COM security settings, GCM prints a +warning message and won't be able to use the broker. ```text warning: broker initialization failed @@ -116,7 +166,23 @@ In order to fix the problem, there are a few options: 1. Run Git or Git Credential Manager from non-elevated processes. 2. Disable the broker by setting the - [`GCM_MSAUTH_USEBROKER`](environment.md#gcm_msauth_usebroker) + [`GCM_MSAUTH_USEBROKER`][GCM_MSAUTH_USEBROKER] environment variable or the - [`credential.msauthUseBroker`](configuration.md#credentialmsauthusebroker) + [`credential.msauthUseBroker`][credential.msauthUseBroker] Git configuration setting to `false`. + +[azure-refresh-token-terms]: https://docs.microsoft.com/azure/active-directory/devices/concept-primary-refresh-token#key-terminology-and-components +[azure-conditional-access]: https://docs.microsoft.com/azure/active-directory/conditional-access/overview +[azure-devops]: https://dev.azure.com +[GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER +[credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker +[aad-questions]: img/aad-questions.png +[aad-questions-21h1]: img/aad-questions-21H1.png +[aad-bitlocker]: img/aad-bitlocker.png +[aad-work-school]: img/aad-work-school.png +[aad-disconnect]: img/aad-disconnect.png +[ms-sign-in]: img/get-signed-in.png +[all-ms-apps]: img/all-microsoft.png +[apps-must-ask]: img/apps-must-ask.png +[ms-com]: https://docs.microsoft.com/en-us/windows/win32/com/the-component-object-model +[msal-dotnet]: https://aka.ms/msal-net diff --git a/docs/wsl.md b/docs/wsl.md index 35e2c6bf7..f37ff614c 100644 --- a/docs/wsl.md +++ b/docs/wsl.md @@ -1,7 +1,7 @@ # Windows Subsystem for Linux (WSL) GCM can be used with the -[Windows Subsystem for Linux (WSL)](https://aka.ms/wsl), both WSL1 and WSL2, by +[Windows Subsystem for Linux (WSL)][wsl], both WSL1 and WSL2, by following these instructions. In order to use GCM with WSL you must be on Windows 10 Version 1903 or later. @@ -12,17 +12,17 @@ It is highly recommended that you install Git for Windows to both install GCM and enable the best experience sharing credentials & settings between WSL and the Windows host. Alternatively, you must be using GCM version 2.0.XXX or later and configure the `WSLENV` environment variable as -[described below](#configuring-wsl-without-git-for-windows). +[described below][configuring-wsl-without-git-for-windows]. ## Configuring WSL with Git for Windows (recommended) -Start by installing the [latest Git for Windows ⬇️](https://github.com/git-for-windows/git/releases/latest) +Start by installing the [latest Git for Windows ⬇️][latest-git-for-windows] _Inside your WSL installation_, run the following command to set GCM as the Git credential helper: ```shell -git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager-core.exe" +git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe" ``` If you intend to use Azure DevOps you must _also_ set the following Git @@ -38,13 +38,13 @@ If you wish to use GCM inside of WSL _without installing Git for Windows_ you must complete additional configuration so that GCM can callback to Git inside of your WSL installation. -Start by installing the [latest GCM ⬇️](https://aka.ms/gcm/latest) +Start by installing the [latest GCM ⬇️][latest-gcm] _Inside your WSL installation_, run the following command to set GCM as the Git credential helper: ```shell -git config --global credential.helper "/mnt/c/Program\ Files\ \(x86\)/Git\ Credential\ Manager/git-credential-manager-core.exe" +git config --global credential.helper "/mnt/c/Program\ Files\ \(x86\)/Git\ Credential\ Manager/git-credential-manager.exe" # For Azure DevOps support only git config --global credential.https://dev.azure.com.useHttpPath true @@ -64,13 +64,14 @@ After updating the `WSLENV` environment variable, restart your WSL installation. If you have installed GCM using the user-only installer (i.e, the `gcmuser-*.exe` installer and not the system-wide/admin required installer), you need to modify -the above instructions to point to `/mnt/c/Users//AppData/Local/Programs/Git\ Credential\ Manager\ Core/git-credential-manager-core.exe` +the above instructions to point to +`/mnt/c/Users//AppData/Local/Programs/Git\ Credential\ Manager\ Core/git-credential-manager.exe` instead. ## How it works GCM leverages the built-in interoperability between Windows and WSL, provided by -Microsoft. You can read more about Windows/WSL interop [here](https://docs.microsoft.com/en-us/windows/wsl/interop). +Microsoft. You can read more about Windows/WSL interop [here][wsl-interop]. Git inside of a WSL installation can launch the GCM _Windows_ application transparently to acquire credentials. Running GCM as a Windows application @@ -93,7 +94,7 @@ as well as WSL Git as they are stored in different files (`%USERPROFILE%\.gitconfig` vs `\\wsl$\distro\home\$USER\.gitconfig`). You can configure WSL such that GCM will use the WSL Git configuration following -the [instructions above](#configuring-wsl-without-git-for-windows). However, +the [instructions above][configuring-wsl-without-git-for-windows]. However, this then means that things like proxy settings are unique to the specific WSL installation, and not shared with others or the Windows host. @@ -102,8 +103,16 @@ installation, and not shared with others or the Windows host. Yes. Rather than install GCM as a Windows application (and have WSL Git invoke the Windows GCM), can you install GCM as a Linux application instead. -To do this, simply follow the [GCM installation instructions for Linux](../README.md#linux). +To do this, simply follow the +[GCM installation instructions for Linux][linux-installation]. **Note:** In this scenario, because GCM is running as a Linux application it cannot utilize authentication or credential storage features of the host Windows operating system. + +[wsl]: https://aka.ms/wsl +[configuring-wsl-without-git-for-windows]: #configuring-wsl-without-git-for-windows +[latest-git-for-windows]: https://github.com/git-for-windows/git/releases/latest +[latest-gcm]: https://aka.ms/gcm/latest +[wsl-interop]: https://docs.microsoft.com/en-us/windows/wsl/interop +[linux-installation]: ../README.md#linux diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj index edb7ef314..e14eef0a4 100644 --- a/src/linux/Packaging.Linux/Packaging.Linux.csproj +++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index a9430850d..4c186a509 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -47,6 +47,7 @@ ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )" SRC="$ROOT/src" OUT="$ROOT/out" GCM_SRC="$SRC/shared/Git-Credential-Manager" +GCM_UI_SRC="$SRC/shared/Git-Credential-Manager.UI.Avalonia" BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI.Avalonia" GITHUB_UI_SRC="$SRC/shared/GitHub.UI.Avalonia" GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia" @@ -120,6 +121,15 @@ $DOTNET_ROOT/dotnet publish "$GCM_SRC" \ -p:PublishSingleFile=true \ --output="$(make_absolute "$PAYLOAD")" || exit 1 +echo "Publishing core UI helper..." +$DOTNET_ROOT/dotnet publish "$GCM_UI_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --runtime="$RUNTIME" \ + --self-contained=true \ + -p:PublishSingleFile=true \ + --output="$(make_absolute "$PAYLOAD")" || exit 1 + echo "Publishing Bitbucket UI helper..." $DOTNET_ROOT/dotnet publish "$BITBUCKET_UI_SRC" \ --configuration="$CONFIGURATION" \ @@ -220,8 +230,14 @@ mkdir -p "$INSTALL_TO" "$LINK_TO" cp -R "$PAYLOAD"/* "$INSTALL_TO" || exit 1 # Create symlink +if [ ! -f "$LINK_TO/git-credential-manager" ]; then + ln -s -r "$INSTALL_TO/git-credential-manager" \ + "$LINK_TO/git-credential-manager" || exit 1 +fi + +# Create legacy symlink with older name if [ ! -f "$LINK_TO/git-credential-manager-core" ]; then - ln -s -r "$INSTALL_TO/git-credential-manager-core" \ + ln -s -r "$INSTALL_TO/git-credential-manager" \ "$LINK_TO/git-credential-manager-core" || exit 1 fi diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index cf57321f9..b563073b3 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -1,9 +1,9 @@ #!/bin/sh -# halt execution immediately on failure -# note there are some scenarios in which this will not exit; -# see https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html -# for additional details +# Halt execution immediately on failure. +# Note there are some scenarios in which this will not exit; see +# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html +# for additional details. set -e is_ci= @@ -11,13 +11,13 @@ for i in "$@"; do case "$i" in -y) is_ci=true - shift # past argument=value + shift # Past argument=value ;; esac done -# in non-ci scenarios, advertise what we will be doing and -# give user the option to exit +# In non-ci scenarios, advertise what we will be doing and +# give user the option to exit. if [ -z $is_ci ]; then echo "This script will download, compile, and install Git Credential Manager to: @@ -47,7 +47,7 @@ install_shared_packages() { local shared_packages="git curl" for package in $shared_packages; do - # ensure we don't stomp on existing installations + # Ensure we don't stomp on existing installations. if [ ! -z $(which $package) ]; then continue fi @@ -66,8 +66,8 @@ ensure_dotnet_installed() { chmod +x ./dotnet-install.sh bash -c "./dotnet-install.sh" - # since we have to run the dotnet install script with bash, dotnet isn't added - # to the process PATH, so we manually add it here + # Since we have to run the dotnet install script with bash, dotnet isn't + # added to the process PATH, so we manually add it here. cd ~ export DOTNET_ROOT=$(pwd)/.dotnet add_to_PATH $DOTNET_ROOT @@ -75,10 +75,10 @@ ensure_dotnet_installed() { } verify_existing_dotnet_installation() { - # get initial pieces of installed sdk version(s) + # Get initial pieces of installed sdk version(s). sdks=$(dotnet --list-sdks | cut -c 1-3) - # if we have a supported version installed, return + # If we have a supported version installed, return. supported_dotnet_versions="6.0" for v in $supported_dotnet_versions; do if [ $(echo $sdks | grep "$v") ]; then @@ -90,7 +90,7 @@ verify_existing_dotnet_installation() { add_to_PATH () { for directory; do if [ ! -d "$directory" ]; then - continue; # skip nonexistent directory + continue; # Skip nonexistent directory. fi case ":$PATH:" in *":$directory:"*) @@ -103,10 +103,17 @@ add_to_PATH () { done } +apt_install() { + pkg_name=$1 + + $sudo_cmd apt update + $sudo_cmd apt install $pkg_name -y 2>/dev/null +} + sudo_cmd= -# if the user isn't root, we need to use `sudo` for certain commands -# (e.g. installing packages) +# If the user isn't root, we need to use `sudo` for certain commands +# (e.g. installing packages). if [ -z "$sudo_cmd" ]; then if [ `id -u` != 0 ]; then sudo_cmd=sudo @@ -120,29 +127,34 @@ case "$distribution" in $sudo_cmd apt update install_shared_packages apt install - # add dotnet package repository/signing key - $sudo_cmd apt update && $sudo_cmd apt install wget -y - curl -LO https://packages.microsoft.com/config/"$distribution"/"$version"/packages-microsoft-prod.deb - $sudo_cmd dpkg -i packages-microsoft-prod.deb - rm packages-microsoft-prod.deb - - # proactively install tzdata to prevent prompts - export DEBIAN_FRONTEND=noninteractive - $sudo_cmd apt install -y --no-install-recommends tzdata - - # install dotnet packages and dependencies if needed + # Install dotnet packages and dependencies if needed. if [ -z "$(verify_existing_dotnet_installation)" ]; then - $sudo_cmd apt update - $sudo_cmd apt install apt-transport-https -y - $sudo_cmd apt update - $sudo_cmd apt install dotnet-sdk-6.0 dpkg-dev -y + # First try to use native feeds (Ubuntu 22.04 and later). + if ! apt_install dotnet6; then + # If the native feeds fail, we fall back to + # packages.microsoft.com. We begin by adding the dotnet package + # repository/signing key. + $sudo_cmd apt update && $sudo_cmd apt install wget -y + curl -LO https://packages.microsoft.com/config/"$distribution"/"$version"/packages-microsoft-prod.deb + $sudo_cmd dpkg -i packages-microsoft-prod.deb + rm packages-microsoft-prod.deb + + # Proactively install tzdata to prevent prompts. + export DEBIAN_FRONTEND=noninteractive + $sudo_cmd apt install -y --no-install-recommends tzdata + + $sudo_cmd apt update + $sudo_cmd apt install apt-transport-https -y + $sudo_cmd apt update + $sudo_cmd apt install dotnet-sdk-6.0 dpkg-dev -y + fi fi ;; linuxmint) $sudo_cmd apt update install_shared_packages apt install - # install dotnet packages and dependencies + # Install dotnet packages and dependencies. $sudo_cmd apt install libc6 libgcc1 libgssapi-krb5-2 libssl1.1 libstdc++6 zlib1g libicu66 -y ensure_dotnet_installed ;; @@ -150,7 +162,7 @@ case "$distribution" in $sudo_cmd dnf update -y install_shared_packages dnf install - # install dotnet/gcm dependencies + # Install dotnet/GCM dependencies. $sudo_cmd dnf install krb5-libs libicu openssl-libs zlib findutils which bash -y ensure_dotnet_installed @@ -159,7 +171,7 @@ case "$distribution" in $sudo_cmd apk update install_shared_packages apk add - # install dotnet/gcm dependencies + # Install dotnet/GCM dependencies. $sudo_cmd apk add icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib which bash coreutils gcompat ensure_dotnet_installed @@ -170,7 +182,7 @@ case "$distribution" in ;; esac -# detect if the script is part of a full source checkout or standalone instead +# Detect if the script is part of a full source checkout or standalone instead. script_path="$(cd "$(dirname "$0")" && pwd)" toplevel_path="${script_path%/src/linux/Packaging.Linux}" if [ "z$script_path" = "z$toplevel_path" ] || [ ! -f "$toplevel_path/Git-Credential-Manager.sln" ]; then diff --git a/src/osx/Installer.Mac/Installer.Mac.csproj b/src/osx/Installer.Mac/Installer.Mac.csproj index f42bd3333..dd0f33ea8 100644 --- a/src/osx/Installer.Mac/Installer.Mac.csproj +++ b/src/osx/Installer.Mac/Installer.Mac.csproj @@ -13,8 +13,10 @@ - - + + + + diff --git a/src/osx/Installer.Mac/build.sh b/src/osx/Installer.Mac/build.sh index e52419601..ce877e6b7 100755 --- a/src/osx/Installer.Mac/build.sh +++ b/src/osx/Installer.Mac/build.sh @@ -61,7 +61,7 @@ OUTDIR="$INSTALLER_OUT/pkg/$CONFIGURATION" PAYLOAD="$OUTDIR/payload" COMPONENTDIR="$OUTDIR/components" COMPONENTOUT="$COMPONENTDIR/com.microsoft.gitcredentialmanager.component.pkg" -DISTOUT="$OUTDIR/gcm-osx-x64-$VERSION.pkg" +DISTOUT="$OUTDIR/gcm-$RUNTIME-$VERSION.pkg" # Layout and pack "$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --output="$PAYLOAD" --runtime="$RUNTIME" || exit 1 diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh index b9991713d..0dc664338 100755 --- a/src/osx/Installer.Mac/layout.sh +++ b/src/osx/Installer.Mac/layout.sh @@ -21,6 +21,7 @@ SRC="$ROOT/src" OUT="$ROOT/out" INSTALLER_SRC="$SRC/osx/Installer.Mac" GCM_SRC="$SRC/shared/Git-Credential-Manager" +GCM_UI_SRC="$SRC/shared/Git-Credential-Manager.UI.Avalonia" BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI.Avalonia" GITHUB_UI_SRC="$SRC/shared/GitHub.UI.Avalonia" GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia" @@ -96,8 +97,14 @@ cp "$INSTALLER_SRC/uninstall.sh" "$PAYLOAD" || exit 1 # Publish core application executables echo "Publishing core application..." dotnet publish "$GCM_SRC" \ - --no-restore \ - -m:1 \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --runtime="$RUNTIME" \ + --self-contained \ + --output="$(make_absolute "$PAYLOAD")" || exit 1 + +echo "Publishing core UI helper..." +dotnet publish "$GCM_UI_SRC" \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -106,8 +113,6 @@ dotnet publish "$GCM_SRC" \ echo "Publishing Bitbucket UI helper..." dotnet publish "$BITBUCKET_UI_SRC" \ - --no-restore \ - -m:1 \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -116,8 +121,6 @@ dotnet publish "$BITBUCKET_UI_SRC" \ echo "Publishing GitHub UI helper..." dotnet publish "$GITHUB_UI_SRC" \ - --no-restore \ - -m:1 \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -126,8 +129,6 @@ dotnet publish "$GITHUB_UI_SRC" \ echo "Publishing GitLab UI helper..." dotnet publish "$GITLAB_UI_SRC" \ - --no-restore \ - -m:1 \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ diff --git a/src/osx/Installer.Mac/resources/en.lproj/conclusion.html b/src/osx/Installer.Mac/resources/en.lproj/conclusion.html index 4554a9283..7c82a0396 100644 --- a/src/osx/Installer.Mac/resources/en.lproj/conclusion.html +++ b/src/osx/Installer.Mac/resources/en.lproj/conclusion.html @@ -19,11 +19,11 @@

Other users

GCM has already been automatically configured for use by the current user with Git. If other users wish to use GCM, have them run the following command to update their global Git configuration (~/.gitconfig):

-

$ git-credential-manager-core configure

+

$ git-credential-manager configure

To configure GCM for all users, run the following command to update the system Git configuration:

-

$ git-credential-manager-core configure --system

+

$ git-credential-manager configure --system

Uninstall

diff --git a/src/osx/Installer.Mac/scripts/postinstall b/src/osx/Installer.Mac/scripts/postinstall index cb73b07db..9e4b0bed5 100755 --- a/src/osx/Installer.Mac/scripts/postinstall +++ b/src/osx/Installer.Mac/scripts/postinstall @@ -28,10 +28,13 @@ fi # Create symlink to GCM in /usr/local/bin mkdir -p /usr/local/bin -/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager-core" /usr/local/bin/git-credential-manager-core +/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager + +# Create legacy symlink to GCMCore in /usr/local/bin +/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager-core # Configure GCM for the current user (running as the current user to avoid root # from taking ownership of ~/.gitconfig) -sudo -u ${USER} "$INSTALL_DESTINATION/git-credential-manager-core" configure +sudo -u ${USER} "$INSTALL_DESTINATION/git-credential-manager" configure exit 0 diff --git a/src/osx/Installer.Mac/uninstall.sh b/src/osx/Installer.Mac/uninstall.sh index f26f2b189..55f6089a9 100755 --- a/src/osx/Installer.Mac/uninstall.sh +++ b/src/osx/Installer.Mac/uninstall.sh @@ -1,7 +1,7 @@ #!/bin/bash THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" -GCMBIN="$THISDIR/git-credential-manager-core" +GCMBIN="$THISDIR/git-credential-manager" # Ensure we're running as root if [ $(id -u) != "0" ] @@ -15,14 +15,23 @@ echo "Unconfiguring credential helper..." sudo -u `/usr/bin/logname` -E "$GCMBIN" unconfigure # Remove symlink -if [ -L /usr/local/bin/git-credential-manager-core ] +if [ -L /usr/local/bin/git-credential-manager ] then echo "Deleting symlink..." - rm /usr/local/bin/git-credential-manager-core + rm /usr/local/bin/git-credential-manager else echo "No symlink found." fi +# Remove legacy symlink +if [ -L /usr/local/bin/git-credential-manager-core ] +then + echo "Deleting legacy symlink..." + rm /usr/local/bin/git-credential-manager-core +else + echo "No legacy symlink found." +fi + # Forget package installation/delete receipt echo "Removing installation receipt..." pkgutil --forget com.microsoft.gitcredentialmanager diff --git a/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj b/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj index 18d7f90d7..4195d936e 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj +++ b/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj @@ -13,7 +13,7 @@ all - + diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs index aace44ad7..e92cb9061 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs @@ -113,7 +113,8 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB { var targetUri = new Uri("https://bitbucket.org"); - var helperPath = "/usr/bin/test-helper"; + var command = "/usr/bin/test-helper"; + var args = ""; var expectedUserName = "jsquire"; var expectedPassword = "password"; var resultDict = new Dictionary @@ -128,7 +129,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection var authMock = new Mock(context) { CallBase = true }; - authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath)) + authMock.Setup(x => x.TryFindHelperCommand(out command, out args)) .Returns(true); authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(resultDict); @@ -140,7 +141,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB Assert.Equal(result.Credential.Account, expectedUserName); Assert.Equal(result.Credential.Password, expectedPassword); - authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None), + authMock.Verify(x => x.InvokeHelperAsync(command, expectedArgs, null, CancellationToken.None), Times.Once); } @@ -149,7 +150,8 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBC { var targetUri = new Uri("https://bitbucket.org"); - var helperPath = "/usr/bin/test-helper"; + var command = "/usr/bin/test-helper"; + var args = ""; var expectedUserName = "jsquire"; var expectedPassword = "password"; var resultDict = new Dictionary @@ -164,7 +166,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBC context.SessionManager.IsDesktopSession = true; // Enable UI helper selection var authMock = new Mock(context) { CallBase = true }; - authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath)) + authMock.Setup(x => x.TryFindHelperCommand(out command, out args)) .Returns(true); authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(resultDict); @@ -176,7 +178,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBC Assert.Equal(result.Credential.Account, expectedUserName); Assert.Equal(result.Credential.Password, expectedPassword); - authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None), + authMock.Verify(x => x.InvokeHelperAsync(command, expectedArgs, null, CancellationToken.None), Times.Once); } @@ -185,7 +187,8 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB { var targetUri = new Uri("https://example.com/bitbucket"); - var helperPath = "/usr/bin/test-helper"; + var command = "/usr/bin/test-helper"; + var args = ""; var expectedUserName = "jsquire"; var expectedPassword = "password"; var resultDict = new Dictionary @@ -200,7 +203,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection var authMock = new Mock(context) { CallBase = true }; - authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath)) + authMock.Setup(x => x.TryFindHelperCommand(out command, out args)) .Returns(true); authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(resultDict); @@ -212,7 +215,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BB Assert.Equal(result.Credential.Account, expectedUserName); Assert.Equal(result.Credential.Password, expectedPassword); - authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None), + authMock.Verify(x => x.InvokeHelperAsync(command, expectedArgs, null, CancellationToken.None), Times.Once); } } diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs new file mode 100644 index 000000000..cad371431 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs @@ -0,0 +1,60 @@ +using System; +using Xunit; + +namespace Atlassian.Bitbucket.Tests +{ + public class BitbucketHelperTest + { + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData("bitbucket.org", true)] + [InlineData("BITBUCKET.ORG", true)] + [InlineData("BiTbUcKeT.OrG", true)] + [InlineData("bitbucket.example.com", false)] + [InlineData("bitbucket.example.org", false)] + [InlineData("bitbucket.org.com", false)] + [InlineData("bitbucket.org.org", false)] + public void BitbucketHelper_IsBitbucketOrg_StringHost(string str, bool expected) + { + bool actual = BitbucketHelper.IsBitbucketOrg(str); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("http://bitbucket.org", true)] + [InlineData("https://bitbucket.org", true)] + [InlineData("http://bitbucket.org/path", true)] + [InlineData("https://bitbucket.org/path", true)] + [InlineData("http://BITBUCKET.ORG", true)] + [InlineData("https://BITBUCKET.ORG", true)] + [InlineData("http://BITBUCKET.ORG/PATH", true)] + [InlineData("https://BITBUCKET.ORG/PATH", true)] + [InlineData("http://BiTbUcKeT.OrG", true)] + [InlineData("https://BiTbUcKeT.OrG", true)] + [InlineData("http://BiTbUcKeT.OrG/pAtH", true)] + [InlineData("https://BiTbUcKeT.OrG/pAtH", true)] + [InlineData("http://bitbucket.example.com", false)] + [InlineData("https://bitbucket.example.com", false)] + [InlineData("http://bitbucket.example.com/path", false)] + [InlineData("https://bitbucket.example.com/path", false)] + [InlineData("http://bitbucket.example.org", false)] + [InlineData("https://bitbucket.example.org", false)] + [InlineData("http://bitbucket.example.org/path", false)] + [InlineData("https://bitbucket.example.org/path", false)] + [InlineData("http://bitbucket.org.com", false)] + [InlineData("https://bitbucket.org.com", false)] + [InlineData("http://bitbucket.org.com/path", false)] + [InlineData("https://bitbucket.org.com/path", false)] + [InlineData("http://bitbucket.org.org", false)] + [InlineData("https://bitbucket.org.org", false)] + [InlineData("http://bitbucket.org.org/path", false)] + [InlineData("https://bitbucket.org.org/path", false)] + public void BitbucketHelper_IsBitbucketOrg_Uri(string str, bool expected) + { + bool actual = BitbucketHelper.IsBitbucketOrg(new Uri(str)); + Assert.Equal(expected, actual); + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index cf8216afc..734ee69b0 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -1,4 +1,6 @@ -using GitCredentialManager; +using Atlassian.Bitbucket.Cloud; +using Atlassian.Bitbucket.DataCenter; +using GitCredentialManager; using GitCredentialManager.Authentication.OAuth; using GitCredentialManager.Tests.Objects; using Moq; @@ -23,58 +25,6 @@ public class BitbucketHostProviderTest private Mock bitbucketAuthentication = new Mock(MockBehavior.Strict); private Mock bitbucketApi = new Mock(MockBehavior.Strict); - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData(" ", false)] - [InlineData("bitbucket.org", true)] - [InlineData("BITBUCKET.ORG", true)] - [InlineData("BiTbUcKeT.OrG", true)] - [InlineData("bitbucket.example.com", false)] - [InlineData("bitbucket.example.org", false)] - [InlineData("bitbucket.org.com", false)] - [InlineData("bitbucket.org.org", false)] - public void BitbucketHostProvider_IsBitbucketOrg_StringHost(string str, bool expected) - { - bool actual = BitbucketHostProvider.IsBitbucketOrg(str); - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("http://bitbucket.org", true)] - [InlineData("https://bitbucket.org", true)] - [InlineData("http://bitbucket.org/path", true)] - [InlineData("https://bitbucket.org/path", true)] - [InlineData("http://BITBUCKET.ORG", true)] - [InlineData("https://BITBUCKET.ORG", true)] - [InlineData("http://BITBUCKET.ORG/PATH", true)] - [InlineData("https://BITBUCKET.ORG/PATH", true)] - [InlineData("http://BiTbUcKeT.OrG", true)] - [InlineData("https://BiTbUcKeT.OrG", true)] - [InlineData("http://BiTbUcKeT.OrG/pAtH", true)] - [InlineData("https://BiTbUcKeT.OrG/pAtH", true)] - [InlineData("http://bitbucket.example.com", false)] - [InlineData("https://bitbucket.example.com", false)] - [InlineData("http://bitbucket.example.com/path", false)] - [InlineData("https://bitbucket.example.com/path", false)] - [InlineData("http://bitbucket.example.org", false)] - [InlineData("https://bitbucket.example.org", false)] - [InlineData("http://bitbucket.example.org/path", false)] - [InlineData("https://bitbucket.example.org/path", false)] - [InlineData("http://bitbucket.org.com", false)] - [InlineData("https://bitbucket.org.com", false)] - [InlineData("http://bitbucket.org.com/path", false)] - [InlineData("https://bitbucket.org.com/path", false)] - [InlineData("http://bitbucket.org.org", false)] - [InlineData("https://bitbucket.org.org", false)] - [InlineData("http://bitbucket.org.org/path", false)] - [InlineData("https://bitbucket.org.org/path", false)] - public void BitbucketHostProvider_IsBitbucketOrg_Uri(string str, bool expected) - { - bool actual = BitbucketHostProvider.IsBitbucketOrg(new Uri(str)); - Assert.Equal(expected, actual); - } - [Theory] [InlineData("https", null, false)] // We report that we support unencrypted HTTP here so that we can fail and @@ -147,10 +97,15 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_Basic( var context = new TestCommandContext(); + if (DC_SERVER_HOST.Equals(host)) + { + MockDCSSOEnabled(); + } MockStoredAccount(context, input, password); MockRemoteBasicValid(input, password); + // HACK rebase MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password, username); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -158,22 +113,20 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_Basic( Assert.Equal(password, credential.Password); // Verify bitbucket.org credentials were validated - if (BITBUCKET_DOT_ORG_HOST.Equals(host)) - { VerifyValidateBasicAuthCredentialsRan(input, password); - } - else - { // Verify DC/Server credentials were not validated - VerifyValidateBasicAuthCredentialsNeverRan(); - } // Stored credentials so don't ask for more VerifyInteractiveAuthNeverRan(); } + public Mock GetBitbucketApi() + { + return bitbucketApi; + } + [Theory] - // DC/Server does not currently support OAuth + // Cloud [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth( string protocol, string host, string username, string token) @@ -182,10 +135,14 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth( var context = new TestCommandContext(); + if (DC_SERVER_HOST.Equals(host)) + { + MockDCSSOEnabled(); + } MockStoredAccount(context, input, token); MockRemoteAccessTokenValid(input, token); - - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -199,6 +156,12 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth( VerifyInteractiveAuthNeverRan(); } + private void MockDCSSOEnabled() + { + bitbucketApi.Setup(ba => ba.GetAuthenticationMethodsAsync()).Returns(Task.FromResult(new List(){AuthenticationMethod.BasicAuth, AuthenticationMethod.Sso})); + bitbucketApi.Setup(ba => ba.IsOAuthInstalledAsync()).Returns(Task.FromResult(true)); + } + [Theory] // DC [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] @@ -214,7 +177,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_Basic( MockPromptBasic(input, password); MockRemoteBasicValid(input, password); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -238,7 +201,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_OAuth( MockRemoteOAuthTokenCreate(input, accessToken, refreshToken); MockRemoteAccessTokenValid(input, accessToken); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -264,9 +227,9 @@ public async Task BitbucketHostProvider_GetCredentialAsync_MissingAT_OAuth_Refre // AT has does not exist, but RT is still valid MockStoredRefreshToken(context, input, refreshToken); MockRemoteAccessTokenValid(input, accessToken); - MockRemoteRefreshTokenValid(refreshToken, accessToken); + MockRemoteRefreshTokenValid(input, refreshToken, accessToken); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -274,7 +237,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_MissingAT_OAuth_Refre Assert.Equal(accessToken, credential.Password); VerifyValidateAccessTokenRan(input, accessToken); - VerifyOAuthRefreshRan(refreshToken); + VerifyOAuthRefreshRan(input, refreshToken); VerifyInteractiveAuthNeverRan(); } @@ -294,9 +257,9 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ExpiredAT_OAuth_Refre MockStoredRefreshToken(context, input, refreshToken); MockRemoteAccessTokenValid(input, accessToken); - MockRemoteRefreshTokenValid(refreshToken, accessToken); + MockRemoteRefreshTokenValid(input, refreshToken, accessToken); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -304,7 +267,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ExpiredAT_OAuth_Refre Assert.Equal(accessToken, credential.Password); VerifyValidateAccessTokenRan(input, accessToken); - VerifyOAuthRefreshRan(refreshToken); + VerifyOAuthRefreshRan(input, refreshToken); VerifyInteractiveAuthNeverRan(); } @@ -322,20 +285,19 @@ public async Task BitbucketHostProvider_GetCredentialAsync_PreconfiguredMode_OAu // We have a stored RT so we can just use that without any prompts MockStoredRefreshToken(context, input, refreshToken); MockRemoteAccessTokenValid(input, accessToken); - MockRemoteRefreshTokenValid(refreshToken, accessToken); + MockRemoteRefreshTokenValid(input, refreshToken, accessToken); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); Assert.NotNull(credential); VerifyInteractiveAuthNeverRan(); - VerifyOAuthRefreshRan(refreshToken); + VerifyOAuthRefreshRan(input, refreshToken); } [Theory] - // DC/Server does not currently support OAuth [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_ACCESS_TOKEN, MOCK_ACCESS_TOKEN_ALT, MOCK_REFRESH_TOKEN)] public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_OAuth_IsRespected( string protocol, string host, string username, string storedToken, string newToken, string refreshToken) @@ -350,9 +312,9 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti MockStoredAccount(context, input, storedToken); MockStoredRefreshToken(context, input, refreshToken); MockRemoteAccessTokenValid(input, newToken); - MockRemoteRefreshTokenValid(refreshToken, newToken); + MockRemoteRefreshTokenValid(input, refreshToken, newToken); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -360,7 +322,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti Assert.Equal(newToken, credential.Password); VerifyInteractiveAuthNeverRan(); - VerifyOAuthRefreshRan(refreshToken); + VerifyOAuthRefreshRan(input, refreshToken); } [Theory] @@ -381,7 +343,7 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti MockStoredAccount(context, input, storedPassword); MockPromptBasic(input, freshPassword); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); var credential = await provider.GetCredentialAsync(input); @@ -392,21 +354,21 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti } [Theory] - // DC - supports Basic - [InlineData("https://example.com", "basic", AuthenticationModes.Basic)] - [InlineData("https://example.com", "oauth", AuthenticationModes.Basic)] + // DC - supports Basic, OAuth + [InlineData("https", "example.com", "basic", AuthenticationModes.Basic)] + [InlineData("https", "example.com", "oauth", AuthenticationModes.OAuth)] + [InlineData("https", "example.com", "NOT-A-REAL-VALUE", DataCenterConstants.ServerAuthenticationModes)] + [InlineData("https", "example.com", "none", DataCenterConstants.ServerAuthenticationModes)] + [InlineData("https", "example.com", null, DataCenterConstants.ServerAuthenticationModes)] // Cloud - supports Basic, OAuth - [InlineData("https://bitbucket.org", "oauth", AuthenticationModes.OAuth)] - [InlineData("https://bitbucket.org", "basic", AuthenticationModes.Basic)] - [InlineData("https://bitbucket.org", "NOT-A-REAL-VALUE", BitbucketConstants.DotOrgAuthenticationModes)] - [InlineData("https://Bitbucket.org", "NOT-A-REAL-VALUE", BitbucketConstants.DotOrgAuthenticationModes)] - [InlineData("https://bitbucket.org", "none", BitbucketConstants.DotOrgAuthenticationModes)] - [InlineData("https://Bitbucket.org", "none", BitbucketConstants.DotOrgAuthenticationModes)] - [InlineData("https://bitbucket.org", null, BitbucketConstants.DotOrgAuthenticationModes)] - [InlineData("https://Bitbucket.org", null, BitbucketConstants.DotOrgAuthenticationModes)] - public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriString, string bitbucketAuthModes, AuthenticationModes expectedModes) - { - var targetUri = new Uri(uriString); + [InlineData("https", "bitbucket.org", "oauth", AuthenticationModes.OAuth)] + [InlineData("https", "bitbucket.org", "basic", AuthenticationModes.Basic)] + [InlineData("https", "bitbucket.org", "NOT-A-REAL-VALUE", CloudConstants.DotOrgAuthenticationModes)] + [InlineData("https", "bitbucket.org", "none", CloudConstants.DotOrgAuthenticationModes)] + [InlineData("https", "bitbucket.org", null, CloudConstants.DotOrgAuthenticationModes)] + public async Task BitbucketHostProvider_GetSupportedAuthenticationModes(string protocol, string host, string bitbucketAuthModes, AuthenticationModes expectedModes) + { + var input = MockInput(protocol, host, null); var context = new TestCommandContext(); if (bitbucketAuthModes != null) @@ -414,9 +376,9 @@ public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriStri context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, bitbucketAuthModes); } - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); - AuthenticationModes actualModes = provider.GetSupportedAuthenticationModes(targetUri); + AuthenticationModes actualModes = await provider.GetSupportedAuthenticationModesAsync(input); Assert.Equal(expectedModes, actualModes); } @@ -429,7 +391,7 @@ public async Task BitbucketHostProvider_StoreCredentialAsync(string protocol, st var context = new TestCommandContext(); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); Assert.Equal(0, context.CredentialStore.Count); @@ -448,7 +410,7 @@ public async Task BitbucketHostProvider_EraseCredentialAsync(string protocol, st MockStoredAccount(context, input, password); - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object); Assert.Equal(1, context.CredentialStore.Count); @@ -473,10 +435,8 @@ private static InputArguments MockInput(string protocol, string host, string use private void VerifyOAuthFlowRan(InputArguments input, string token) { - var remoteUri = input.GetRemoteUri(); - // Get new access token and refresh token - bitbucketAuthentication.Verify(m => m.CreateOAuthCredentialsAsync(remoteUri), Times.Once); + bitbucketAuthentication.Verify(m => m.CreateOAuthCredentialsAsync(input), Times.Once); // Check access token works/resolve username bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once); @@ -512,15 +472,15 @@ private void VerifyInteractiveAuthNeverRan() bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } - private void VerifyOAuthRefreshRan(string refreshToken) + private void VerifyOAuthRefreshRan(InputArguments input, string refreshToken) { // Check refresh was called - bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(refreshToken), Times.Once); + bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(input, refreshToken), Times.Once); } - private void MockRemoteRefreshTokenValid(string refreshToken, string accessToken) + private void MockRemoteRefreshTokenValid(InputArguments input, string refreshToken, string accessToken) { - bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(refreshToken)).ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token")); + bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(input, refreshToken)).ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token")); } private void MockPromptBasic(InputArguments input, string password) @@ -539,35 +499,37 @@ private void MockPromptOAuth(InputArguments input) private void MockRemoteBasicValid(InputArguments input, string password, bool twoFactor = true) { - var userInfo = new UserInfo - { - UserName = input.UserName, - IsTwoFactorAuthenticationEnabled = twoFactor - }; + var userInfo = new Mock(MockBehavior.Strict); + userInfo.Setup(ui => ui.IsTwoFactorAuthenticationEnabled).Returns(twoFactor); + userInfo.Setup(ui => ui.UserName).Returns(input.UserName); // Basic bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); + .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo.Object)); } private void MockRemoteAccessTokenExpired(InputArguments input, string token) { // OAuth bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized)); + .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized)); } private void MockRemoteAccessTokenValid(InputArguments input, string token, bool twoFactor = true) { - var userInfo = new UserInfo - { - UserName = input.UserName, - IsTwoFactorAuthenticationEnabled = twoFactor - }; + var userInfo = new Mock(MockBehavior.Strict); + userInfo.Setup(ui => ui.IsTwoFactorAuthenticationEnabled).Returns(twoFactor); + userInfo.Setup(ui => ui.UserName).Returns(input.UserName); // OAuth bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); + .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo.Object)); + } + + private static void MockRemoteOAuthAccountIsInvalid(Mock bitbucketApi) + { + // OAuth + bitbucketApi.Setup(x => x.GetUserInformationAsync(null, It.IsAny(), true)).ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.BadRequest)); } private static void MockStoredAccount(TestCommandContext context, InputArguments input, string password) @@ -586,8 +548,7 @@ private static void MockStoredRefreshToken(TestCommandContext context, InputArgu private void MockRemoteOAuthTokenCreate(InputArguments input, string accessToken, string refreshToken) { - var remoteUri = input.GetRemoteUri(); - bitbucketAuthentication.Setup(x => x.CreateOAuthCredentialsAsync(remoteUri)) + bitbucketAuthentication.Setup(x => x.CreateOAuthCredentialsAsync(input)) .ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token") { RefreshToken = refreshToken }); } @@ -601,6 +562,15 @@ private void VerifyOAuthRefreshTokenStored(TestCommandContext context, InputArgu Assert.Equal(refreshToken, credential.Password); } + private static Mock> MockRestApiRegistry(InputArguments input, Mock bitbucketApi) + { + var restApiRegistry = new Mock>(MockBehavior.Strict); + + restApiRegistry.Setup(rar => rar.Get(input)).Returns(bitbucketApi.Object); + + return restApiRegistry; + } + #endregion } } diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs new file mode 100644 index 000000000..ee4499dfb --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using GitCredentialManager; +using Moq; +using Xunit; + +namespace Atlassian.Bitbucket.Tests +{ + public class BitbucketRestApiRegistryTest + { + private Mock context = new Mock(MockBehavior.Strict); + private Mock settings = new Mock(MockBehavior.Strict); + + [Fact] + public void BitbucketRestApiRegistry_Get_ReturnsCloudApi_ForBitbucketOrg() + { + // Given + settings.Setup(s => s.RemoteUri).Returns(new System.Uri("https://bitbucket.org")); + context.Setup(c => c.Settings).Returns(settings.Object); + + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "bitbucket.org", + }); + + // When + var registry = new BitbucketRestApiRegistry(context.Object); + var api = registry.Get(input); + + // Then + Assert.NotNull(api); + Assert.IsType(api); + + } + + [Fact] + public void BitbucketRestApiRegistry_Get_ReturnsDataCenterApi_ForBitbucketDC() + { + // Given + settings.Setup(s => s.RemoteUri).Returns(new System.Uri("https://example.com")); + context.Setup(c => c.Settings).Returns(settings.Object); + + var input = new InputArguments(new Dictionary + { + ["protocol"] = "http", + ["host"] = "example.com", + }); + + // When + var registry = new BitbucketRestApiRegistry(context.Object); + var api = registry.Get(input); + + // Then + Assert.NotNull(api); + Assert.IsType(api); + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs new file mode 100644 index 000000000..02b7f35ec --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using Xunit; + +namespace Atlassian.Bitbucket.Tests +{ + public class BitbucketTokenEndpointResponseJsonTest + { + [Fact] + public void BitbucketTokenEndpointResponseJson_Deserialize_Scopes_Not_Scope() + { + var scopesString = "a,b,c"; + var json = "{access_token: '', token_type: '', scopes:'" + scopesString + "', scope: 'x,y,z'}"; + + var result = JsonConvert.DeserializeObject(json); + + Assert.Equal(scopesString, result.Scope); + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketOauth2ClientTest.cs b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs similarity index 67% rename from src/shared/Atlassian.Bitbucket.Tests/BitbucketOauth2ClientTest.cs rename to src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs index 699081a94..a1afb8f62 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketOauth2ClientTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs @@ -4,17 +4,19 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Atlassian.Bitbucket.Cloud; using GitCredentialManager; using GitCredentialManager.Authentication.OAuth; using Moq; using Xunit; -namespace Atlassian.Bitbucket.Tests +namespace Atlassian.Bitbucket.Tests.Cloud { public class BitbucketOAuth2ClientTest { private Mock httpClient = new Mock(MockBehavior.Strict); private Mock settings = new Mock(MockBehavior.Loose); + private Mock trace = new Mock(MockBehavior.Loose); private Mock browser = new Mock(MockBehavior.Strict); private Mock codeGenerator = new Mock(MockBehavior.Strict); private IEnumerable scopes = new List(); @@ -32,13 +34,13 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_ReturnsCode() Uri finalCallbackUri = MockFinalCallbackUri(); - MockGetAuthenticationCodeAsync(finalCallbackUri, null); + Bitbucket.Cloud.BitbucketOAuth2Client client = GetBitbucketOAuth2Client(); - MockCodeGenerator(); + MockGetAuthenticationCodeAsync(finalCallbackUri, null, client.Scopes); - BitbucketOAuth2Client client = GetBitbucketOAuth2Client(); + MockCodeGenerator(); - var result = await client.GetAuthorizationCodeAsync(scopes, browser.Object, ct); + var result = await client.GetAuthorizationCodeAsync(browser.Object, ct); VerifyAuthorizationCodeResult(result); } @@ -52,13 +54,13 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient Uri finalCallbackUri = MockFinalCallbackUri(); - MockGetAuthenticationCodeAsync(finalCallbackUri, clientId); + Bitbucket.Cloud.BitbucketOAuth2Client client = GetBitbucketOAuth2Client(); + + MockGetAuthenticationCodeAsync(finalCallbackUri, clientId, client.Scopes); MockCodeGenerator(); - BitbucketOAuth2Client client = GetBitbucketOAuth2Client(); - - var result = await client.GetAuthorizationCodeAsync(scopes, browser.Object, ct); + var result = await client.GetAuthorizationCodeAsync(browser.Object, ct); VerifyAuthorizationCodeResult(result); } @@ -66,22 +68,28 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient [Fact] public async Task BitbucketOAuth2Client_GetDeviceCodeAsync() { - var client = new BitbucketOAuth2Client(httpClient.Object, settings.Object); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct)); } - private void VerifyOAuth2TokenResult(OAuth2TokenResult result) + [Theory] + [InlineData("https", "example.com", "john", "https://example.com/refresh_token")] + [InlineData("http", "example.com", "john", "http://example.com/refresh_token")] + [InlineData("https", "example.com", "dave", "https://example.com/refresh_token")] + [InlineData("https", "example.com/", "john", "https://example.com/refresh_token")] + public void BitbucketOAuth2Client_GetRefreshTokenServiceName(string protocol, string host, string username, string expectedResult) { - Assert.NotNull(result); - IEnumerable access_token = null; - Assert.Equal(access_token, result.AccessToken); - IEnumerable refresh_token = null; - Assert.Equal(refresh_token, result.RefreshToken); - IEnumerable tokenType = null; - Assert.Equal(tokenType, result.TokenType); - Assert.Null(result.Scopes); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); + var input = new InputArguments(new Dictionary + { + ["protocol"] = protocol, + ["host"] = host, + ["username"] = username + }); + Assert.Equal(expectedResult, client.GetRefreshTokenServiceName(input)); } + private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result) { Assert.NotNull(result); @@ -90,9 +98,9 @@ private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result) Assert.Equal(pkceCodeVerifier, result.CodeVerifier); } - private BitbucketOAuth2Client GetBitbucketOAuth2Client() + private Bitbucket.Cloud.BitbucketOAuth2Client GetBitbucketOAuth2Client() { - var client = new BitbucketOAuth2Client(httpClient.Object, settings.Object); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); client.CodeGenerator = codeGenerator.Object; return client; } @@ -104,16 +112,17 @@ private void MockCodeGenerator() codeGenerator.Setup(c => c.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, pkceCodeVerifier)).Returns(pkceCodeChallenge); } - private void MockGetAuthenticationCodeAsync(Uri finalCallbackUri, string overrideClientId) + private void MockGetAuthenticationCodeAsync(Uri finalCallbackUri, string overrideClientId, IEnumerable scopes) { - var authorizationUri = new UriBuilder(BitbucketConstants.OAuth2AuthorizationEndpoint) + var authorizationUri = new UriBuilder(CloudConstants.OAuth2AuthorizationEndpoint) { Query = "?response_type=code" - + "&client_id=" + (overrideClientId ?? BitbucketConstants.OAuth2ClientId) + + "&client_id=" + (overrideClientId ?? CloudConstants.OAuth2ClientId) + "&state=12345" + "&code_challenge_method=" + OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodS256 + "&code_challenge=" + WebUtility.UrlEncode(pkceCodeChallenge).ToLower() + "&redirect_uri=" + WebUtility.UrlEncode(rootCallbackUri.AbsoluteUri).ToLower() + + "&scope=" + WebUtility.UrlEncode(string.Join(" ", scopes)).ToLower() }.Uri; browser.Setup(b => b.GetAuthenticationCodeAsync(authorizationUri, rootCallbackUri, ct)).Returns(Task.FromResult(finalCallbackUri)); @@ -133,8 +142,8 @@ private string MockeClientIdOverride(bool set) private string MockClientIdOverride(bool set, string value) { settings.Setup(s => s.TryGetSetting( - BitbucketConstants.EnvironmentVariables.DevOAuthClientId, - Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.DevOAuthClientId, + CloudConstants.EnvironmentVariables.OAuthClientId, + Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientId, out value)).Returns(set); return value; } diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiTest.cs b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs similarity index 90% rename from src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiTest.cs rename to src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs index 7ed9192e8..c5a97b4a2 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs @@ -2,11 +2,12 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Atlassian.Bitbucket.Cloud; using GitCredentialManager.Tests; using GitCredentialManager.Tests.Objects; using Xunit; -namespace Atlassian.Bitbucket.Tests +namespace Atlassian.Bitbucket.Tests.Cloud { public class BitbucketRestApiTest { @@ -51,9 +52,9 @@ public async Task BitbucketRestApi_GetUserInformationAsync_ReturnsUserInfo_ForSu Assert.NotNull(result); Assert.Equal(username, result.Response.UserName); - Assert.Equal(accountId, result.Response.AccountId); - Assert.Equal(uuid, result.Response.Uuid); Assert.Equal(twoFactorAuthenticationEnabled, result.Response.IsTwoFactorAuthenticationEnabled); + Assert.Equal(accountId, ((UserInfo)result.Response).AccountId); + Assert.Equal(uuid, ((UserInfo)result.Response).Uuid); httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1); } diff --git a/src/shared/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs b/src/shared/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs new file mode 100644 index 000000000..784a60594 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Atlassian.Bitbucket.Cloud; +using Xunit; + +namespace Atlassian.Bitbucket.Tests.Cloud +{ + public class UserInfoTest + { + [Fact] + public void UserInfo_Set() + { + var uuid = System.Guid.NewGuid(); + var userInfo = new UserInfo() + { + AccountId = "abc", + IsTwoFactorAuthenticationEnabled = false, + UserName = "123", + Uuid = uuid + }; + + Assert.Equal("abc", userInfo.AccountId); + Assert.False(userInfo.IsTwoFactorAuthenticationEnabled); + Assert.Equal("123", userInfo.UserName); + Assert.Equal(uuid, userInfo.Uuid); + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs new file mode 100644 index 000000000..97d194764 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Atlassian.Bitbucket.DataCenter; +using GitCredentialManager; +using GitCredentialManager.Authentication.OAuth; +using Moq; +using Xunit; + +namespace Atlassian.Bitbucket.Tests.DataCenter +{ + public class BitbucketOAuth2ClientTest + { + private Mock httpClient = new Mock(MockBehavior.Strict); + private Mock settings = new Mock(MockBehavior.Loose); + private Mock trace = new Mock(MockBehavior.Loose); + private Mock browser = new Mock(MockBehavior.Strict); + private Mock codeGenerator = new Mock(MockBehavior.Strict); + private CancellationToken ct = new CancellationToken(); + private Uri rootCallbackUri = new Uri("http://localhost:34106/"); + private string nonce = "12345"; + private string pkceCodeVerifier = "abcde"; + private string pkceCodeChallenge = "xyz987"; + private string authorization_code = "authorization_token"; + + [Fact] + public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_ReturnsCode() + { + var remoteUrl = MockRemoteUri("http://example.com"); + var clientId = MockClientIdOverride("dc-client-id"); + MockClientSecretOverride("dc-client-seccret"); + + Uri finalCallbackUri = MockFinalCallbackUri(rootCallbackUri); + + var client = GetBitbucketOAuth2Client(); + + MockGetAuthenticationCodeAsync(remoteUrl, rootCallbackUri, finalCallbackUri, clientId, client.Scopes); + + MockCodeGenerator(); + + var result = await client.GetAuthorizationCodeAsync(browser.Object, ct); + + VerifyAuthorizationCodeResult(result, rootCallbackUri); + } + + [Fact] + public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_ReturnsCode_WhileRespectingRedirectUriOverride() + { + var rootCallbackUrl = MockRootCallbackUriOverride("http://localhost:12345/"); + var remoteUrl = MockRemoteUri("http://example.com"); + var clientId = MockClientIdOverride("dc-client-id"); + MockClientSecretOverride("dc-client-seccret"); + + Uri finalCallbackUri = MockFinalCallbackUri(new Uri(rootCallbackUrl)); + + var client = GetBitbucketOAuth2Client(); + + MockGetAuthenticationCodeAsync(remoteUrl, new Uri(rootCallbackUrl), finalCallbackUri, clientId, client.Scopes); + + MockCodeGenerator(); + + var result = await client.GetAuthorizationCodeAsync(browser.Object, ct); + + VerifyAuthorizationCodeResult(result, new Uri(rootCallbackUrl)); + } + + private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result, Uri redirectUri) + { + Assert.NotNull(result); + Assert.Equal(authorization_code, result.Code); + Assert.Equal(redirectUri, result.RedirectUri); + Assert.Equal(pkceCodeVerifier, result.CodeVerifier); + } + + private Bitbucket.DataCenter.BitbucketOAuth2Client GetBitbucketOAuth2Client() + { + var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); + client.CodeGenerator = codeGenerator.Object; + return client; + } + + private void MockCodeGenerator() + { + codeGenerator.Setup(c => c.CreateNonce()).Returns(nonce); + codeGenerator.Setup(c => c.CreatePkceCodeVerifier()).Returns(pkceCodeVerifier); + codeGenerator.Setup(c => c.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, pkceCodeVerifier)).Returns(pkceCodeChallenge); + } + + private void MockGetAuthenticationCodeAsync(string url, Uri redirectUri, Uri finalCallbackUri, string overrideClientId, IEnumerable scopes) + { + var authorizationUri = new UriBuilder(url + "/rest/oauth2/latest/authorize") + { + Query = "?response_type=code" + + "&client_id=" + (overrideClientId ?? "clientId") + + "&state=12345" + + "&code_challenge_method=" + OAuth2Constants.AuthorizationEndpoint.PkceChallengeMethodS256 + + "&code_challenge=" + WebUtility.UrlEncode(pkceCodeChallenge).ToLower() + + "&redirect_uri=" + WebUtility.UrlEncode(redirectUri.AbsoluteUri).ToLower() + + "&scope=" + WebUtility.UrlEncode(string.Join(" ", scopes)).ToUpper() + }.Uri; + + browser.Setup(b => b.GetAuthenticationCodeAsync(authorizationUri, redirectUri, ct)).Returns(Task.FromResult(finalCallbackUri)); + } + + private Uri MockFinalCallbackUri(Uri redirectUri) + { + var finalUri = new Uri(rootCallbackUri, "?state=" + nonce + "&code=" + authorization_code); + // This is a simplification but consistent + browser.Setup(b => b.UpdateRedirectUri(redirectUri)).Returns(redirectUri); + return finalUri; + } + + private string MockRemoteUri(string value) + { + settings.Setup(s => s.RemoteUri).Returns(new Uri(value)); + return value; + } + + private string MockClientIdOverride(string value) + { + settings.Setup(s => s.TryGetSetting( + DataCenterConstants.EnvironmentVariables.OAuthClientId, + Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientId, + out value)).Returns(true); + return value; + } + + private string MockClientSecretOverride(string value) + { + settings.Setup(s => s.TryGetSetting( + DataCenterConstants.EnvironmentVariables.OAuthClientSecret, + Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret, + out value)).Returns(true); + return value; + } + + private string MockRootCallbackUriOverride(string value) + { + settings.Setup(s => s.TryGetSetting( + DataCenterConstants.EnvironmentVariables.OAuthRedirectUri, + Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri, + out value)).Returns(true); + return value; + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs new file mode 100644 index 000000000..b333804c6 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Atlassian.Bitbucket.DataCenter; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace Atlassian.Bitbucket.Tests.DataCenter +{ + public class BitbucketRestApiTest + { + [Fact] + public async Task BitbucketRestApi_GetUserInformationAsync_ReturnsUserInfo_ForSuccessfulRequest_DoesNothing() + { + var twoFactorAuthenticationEnabled = false; + + var context = new TestCommandContext(); + + var expectedRequestUri = new Uri("http://example.com/rest/api/1.0/users"); + var httpHandler = new TestHttpMessageHandler(); + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK); + httpHandler.Setup(HttpMethod.Get, expectedRequestUri, request => + { + return httpResponse; + }); + context.HttpClientFactory.MessageHandler = httpHandler; + + context.Settings.RemoteUri = new Uri("http://example.com"); + + var api = new BitbucketRestApi(context); + var result = await api.GetUserInformationAsync("never used", "never used", false); + + Assert.NotNull(result); + Assert.Equal(DataCenterConstants.OAuthUserName, result.Response.UserName); + Assert.Equal(twoFactorAuthenticationEnabled, result.Response.IsTwoFactorAuthenticationEnabled); + + httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1); + } + + [Theory] + [InlineData(HttpStatusCode.Unauthorized, true)] + [InlineData(HttpStatusCode.NotFound, false)] + public async Task BitbucketRestApi_IsOAuthInstalledAsync_ReflectsBitbucketAuthenticationResponse(HttpStatusCode responseCode, bool impliedSupport) + { + var context = new TestCommandContext(); + var httpHandler = new TestHttpMessageHandler(); + + var expectedRequestUri = new Uri("http://example.com/rest/oauth2/1.0/client"); + + var httpResponse = new HttpResponseMessage(responseCode); + httpHandler.Setup(HttpMethod.Get, expectedRequestUri, request => + { + return httpResponse; + }); + + context.HttpClientFactory.MessageHandler = httpHandler; + context.Settings.RemoteUri = new Uri("http://example.com"); + + var api = new BitbucketRestApi(context); + + var isInstalled = await api.IsOAuthInstalledAsync(); + + httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1); + + Assert.Equal(impliedSupport, isInstalled); + } + + [Theory] + [MemberData(nameof(GetAuthenticationMethodsAsyncData))] + public async Task BitbucketRestApi_GetAuthenticationMethodsAsync_ReflectRestApiResponse(string loginOptionResponseJson, List impliedSupportedMethods, List impliedUnsupportedMethods) + { + var context = new TestCommandContext(); + var httpHandler = new TestHttpMessageHandler(); + + var expectedRequestUri = new Uri("http://example.com/rest/authconfig/1.0/login-options"); + + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(loginOptionResponseJson) + }; + + httpHandler.Setup(HttpMethod.Get, expectedRequestUri, request => + { + return httpResponse; + }); + + context.HttpClientFactory.MessageHandler = httpHandler; + context.Settings.RemoteUri = new Uri("http://example.com"); + + var api = new BitbucketRestApi(context); + + var authMethods = await api.GetAuthenticationMethodsAsync(); + + httpHandler.AssertRequest(HttpMethod.Get, expectedRequestUri, 1); + + Assert.NotNull(authMethods); + Assert.Equal(authMethods.Count, impliedSupportedMethods.Count); + Assert.Contains(authMethods, m => impliedSupportedMethods.Contains(m)); + Assert.DoesNotContain(authMethods, m => impliedUnsupportedMethods.Contains(m)); + } + + public static IEnumerable GetAuthenticationMethodsAsyncData => + new List + { + new object[] { $"{{ \"results\":[ {{ \"type\":\"LOGIN_FORM\"}}]}}", + new List{AuthenticationMethod.BasicAuth}, + new List{AuthenticationMethod.Sso}}, + new object[] { $"{{ \"results\":[{{\"type\":\"IDP\"}}]}}", + new List{AuthenticationMethod.Sso}, + new List{AuthenticationMethod.BasicAuth}}, + new object[] { $"{{ \"results\":[{{\"type\":\"IDP\"}}, {{ \"type\":\"LOGIN_FORM\"}}, {{ \"type\":\"UNDEFINED\"}}]}}", + new List{AuthenticationMethod.Sso, AuthenticationMethod.BasicAuth}, + new List()}, + }; + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs new file mode 100644 index 000000000..6bb6efc85 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Atlassian.Bitbucket.DataCenter; +using Xunit; + +namespace Atlassian.Bitbucket.Tests.DataCenter +{ + public class LoginOptionsTest + { + + [Fact] + public void LoginOptions_Set() + { + var loginOption = new LoginOption() + { + Type = "abc", + Id = 1 + }; + + var results = new List() + { + loginOption + }; + + var loginOptions = new LoginOptions() + { + Results = results + }; + + Assert.NotNull(loginOptions.Results); + Assert.Contains(loginOption, loginOptions.Results); + + Assert.Equal("abc", loginOptions.Results.First().Type); + Assert.Equal(1, loginOptions.Results.First().Id); + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs new file mode 100644 index 000000000..4f1709660 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Atlassian.Bitbucket.DataCenter; +using Xunit; + +namespace Atlassian.Bitbucket.Tests.DataCenter +{ + public class UserInfoTest + { + [Fact] + public void UserInfo_Set() + { + var uuid = System.Guid.NewGuid(); + var userInfo = new UserInfo() + { + UserName = "123" + }; + + Assert.False(userInfo.IsTwoFactorAuthenticationEnabled); + Assert.Equal("123", userInfo.UserName); + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs b/src/shared/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs new file mode 100644 index 000000000..c7bc06917 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Atlassian.Bitbucket.Cloud; +using Atlassian.Bitbucket.DataCenter; +using GitCredentialManager; +using Moq; +using Xunit; + +namespace Atlassian.Bitbucket.Tests +{ + public class OAuth2ClientRegistryTest + { + private Mock context = new Mock(MockBehavior.Loose); + private Mock settings = new Mock(MockBehavior.Strict); + private Mock httpClientFactory = new Mock(MockBehavior.Strict); + private Mock trace = new Mock(MockBehavior.Strict); + + [Fact] + public void BitbucketRestApiRegistry_Get_ReturnsCloudOAuth2Client() + { + var host = "bitbucket.org"; + + // Given + settings.Setup(s => s.RemoteUri).Returns(new System.Uri("https://" + host)); + context.Setup(c => c.Settings).Returns(settings.Object); + MockSettingOverride(CloudConstants.EnvironmentVariables.OAuthClientId, CloudConstants.GitConfiguration.Credential.OAuthClientId, "never used", false); + MockSettingOverride(CloudConstants.EnvironmentVariables.OAuthClientSecret, CloudConstants.GitConfiguration.Credential.OAuthClientSecret, "never used", false); + MockSettingOverride(CloudConstants.EnvironmentVariables.OAuthRedirectUri, CloudConstants.GitConfiguration.Credential.OAuthRedirectUri, "never used", false); + MockHttpClientFactory(); + var input = MockInputArguments(host); + + // When + var registry = new OAuth2ClientRegistry(context.Object); + var api = registry.Get(input); + + // Then + Assert.NotNull(api); + Assert.IsType(api); + + } + + [Fact] + public void BitbucketRestApiRegistry_Get_ReturnsDataCenterOAuth2Client_ForBitbucketDC() + { + var host = "example.com"; + + // Given + settings.Setup(s => s.RemoteUri).Returns(new System.Uri("https://example.com")); + context.Setup(c => c.Settings).Returns(settings.Object); + MockSettingOverride(DataCenterConstants.EnvironmentVariables.OAuthClientId, DataCenterConstants.GitConfiguration.Credential.OAuthClientId, "", true); + MockSettingOverride(DataCenterConstants.EnvironmentVariables.OAuthClientSecret, DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret, "", true); ; + MockSettingOverride(DataCenterConstants.EnvironmentVariables.OAuthRedirectUri, DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri, "never used", false); + MockHttpClientFactory(); + var input = MockInputArguments(host); + + // When + var registry = new OAuth2ClientRegistry(context.Object); + var api = registry.Get(input); + + // Then + Assert.NotNull(api); + Assert.IsType(api); + + } + + private static InputArguments MockInputArguments(string host) + { + return new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = host, + }); + } + + private void MockHttpClientFactory() + { + context.Setup(c => c.HttpClientFactory).Returns(httpClientFactory.Object); + httpClientFactory.Setup(f => f.CreateClient()).Returns(new HttpClient()); + } + + private string MockSettingOverride(string envKey, string configKey, string settingValue, bool isOverridden) + { + settings.Setup(s => s.TryGetSetting( + envKey, + Constants.GitConfiguration.Credential.SectionName, configKey, + out settingValue)).Returns(isOverridden); + return settingValue; + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/AuthenticationMethod.cs b/src/shared/Atlassian.Bitbucket/AuthenticationMethod.cs new file mode 100644 index 000000000..9e108c228 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/AuthenticationMethod.cs @@ -0,0 +1,10 @@ +using System; +namespace Atlassian.Bitbucket +{ + public enum AuthenticationMethod + { + BasicAuth, + Sso + } +} + diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs index 14e7b3fb1..1cacd769e 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Atlassian.Bitbucket.Cloud; using GitCredentialManager; using GitCredentialManager.Authentication; using GitCredentialManager.Authentication.OAuth; @@ -23,8 +24,9 @@ public enum AuthenticationModes public interface IBitbucketAuthentication : IDisposable { Task GetCredentialsAsync(Uri targetUri, string userName, AuthenticationModes modes); - Task CreateOAuthCredentialsAsync(Uri targetUri); - Task RefreshOAuthCredentialsAsync(string refreshToken); + Task CreateOAuthCredentialsAsync(InputArguments input); + Task RefreshOAuthCredentialsAsync(InputArguments input, string refreshToken); + string GetRefreshTokenServiceName(InputArguments input); } public class CredentialsPromptResult @@ -49,17 +51,20 @@ public class BitbucketAuthentication : AuthenticationBase, IBitbucketAuthenticat { public static readonly string[] AuthorityIds = { - "bitbucket", + BitbucketConstants.Id, }; - private static readonly string[] Scopes = - { - BitbucketConstants.OAuthScopes.RepositoryWrite, - BitbucketConstants.OAuthScopes.Account, - }; + private readonly IRegistry _oauth2ClientRegistry; public BitbucketAuthentication(ICommandContext context) - : base(context) { } + : this(context, new OAuth2ClientRegistry(context)) { } + + public BitbucketAuthentication(ICommandContext context, IRegistry oauth2ClientRegistry) + : base(context) + { + EnsureArgument.NotNull(oauth2ClientRegistry, nameof(oauth2ClientRegistry)); + this._oauth2ClientRegistry = oauth2ClientRegistry; + } public async Task GetCredentialsAsync(Uri targetUri, string userName, AuthenticationModes modes) { @@ -88,30 +93,31 @@ public async Task GetCredentialsAsync(Uri targetUri, st // Shell out to the UI helper and show the Bitbucket u/p prompt if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession && - TryFindHelperExecutablePath(out string helperPath)) + TryFindHelperCommand(out string helperCommand, out string args)) { - var cmdArgs = new StringBuilder("prompt"); - if (!BitbucketHostProvider.IsBitbucketOrg(targetUri)) + var promptArgs = new StringBuilder(args); + promptArgs.Append("prompt"); + if (!BitbucketHelper.IsBitbucketOrg(targetUri)) { - cmdArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString())); + promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString())); } if (!string.IsNullOrWhiteSpace(userName)) { - cmdArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName)); + promptArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName)); } if ((modes & AuthenticationModes.Basic) != 0) { - cmdArgs.Append(" --show-basic"); + promptArgs.Append(" --show-basic"); } if ((modes & AuthenticationModes.OAuth) != 0) { - cmdArgs.Append(" --show-oauth"); + promptArgs.Append(" --show-oauth"); } - IDictionary output = await InvokeHelperAsync(helperPath, cmdArgs.ToString()); + IDictionary output = await InvokeHelperAsync(helperCommand, promptArgs.ToString()); if (output.TryGetValue("mode", out string mode) && StringComparer.OrdinalIgnoreCase.Equals(mode, "oauth")) @@ -189,12 +195,10 @@ public async Task GetCredentialsAsync(Uri targetUri, st } } - public async Task CreateOAuthCredentialsAsync(Uri targetUri) + public async Task CreateOAuthCredentialsAsync(InputArguments input) { ThrowIfUserInteractionDisabled(); - var oauthClient = new BitbucketOAuth2Client(HttpClient, Context.Settings); - var browserOptions = new OAuth2WebBrowserOptions { SuccessResponseHtml = BitbucketResources.AuthenticationResponseSuccessHtml, @@ -202,25 +206,32 @@ public async Task CreateOAuthCredentialsAsync(Uri targetUri) }; var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions); - var authCodeResult = await oauthClient.GetAuthorizationCodeAsync(Scopes, browser, CancellationToken.None); + var oauth2Client = _oauth2ClientRegistry.Get(input); - return await oauthClient.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None); + var authCodeResult = await oauth2Client.GetAuthorizationCodeAsync(browser, CancellationToken.None); + return await oauth2Client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None); } - public async Task RefreshOAuthCredentialsAsync(string refreshToken) + public async Task RefreshOAuthCredentialsAsync(InputArguments input, string refreshToken) { - var oauthClient = new BitbucketOAuth2Client(HttpClient, Context.Settings); + var client = _oauth2ClientRegistry.Get(input); + return await client.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None); + } - return await oauthClient.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None); + public string GetRefreshTokenServiceName(InputArguments input) + { + var client = _oauth2ClientRegistry.Get(input); + return client.GetRefreshTokenServiceName(input); } - protected internal virtual bool TryFindHelperExecutablePath(out string path) + protected internal virtual bool TryFindHelperCommand(out string command, out string args) { - return TryFindHelperExecutablePath( + return TryFindHelperCommand( BitbucketConstants.EnvironmentVariables.AuthenticationHelper, BitbucketConstants.GitConfiguration.Credential.AuthenticationHelper, BitbucketConstants.DefaultAuthenticationHelper, - out path); + out command, + out args); } private HttpClient _httpClient; diff --git a/src/shared/Atlassian.Bitbucket/BitbucketConstants.cs b/src/shared/Atlassian.Bitbucket/BitbucketConstants.cs index d995fa276..514f05ea4 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketConstants.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketConstants.cs @@ -4,36 +4,18 @@ namespace Atlassian.Bitbucket { public static class BitbucketConstants { - public const string BitbucketBaseUrlHost = "bitbucket.org"; - public static readonly Uri BitbucketApiUri = new Uri("https://api.bitbucket.org"); - public const string DefaultAuthenticationHelper = "Atlassian.Bitbucket.UI"; - - // TODO: use the GCM client ID and secret once we have this approved. - // Until then continue to use Sourcetree's values like GCM Windows. - //public const string OAuth2ClientId = "b5AKdPfpgFdEGpKzPE"; - //public const string OAuth2ClientSecret = "7NUP5qUtSR3SxdFK4xAGaU6PMNvNdE59"; - //public static readonly Uri OAuth2RedirectUri = new Uri("http://localhost:46337/"); - public const string OAuth2ClientId = "HJdmKXV87DsmC9zSWB"; - public const string OAuth2ClientSecret = "wwWw47VB9ZHwMsD4Q4rAveHkbxNrMp3n"; - public static readonly Uri OAuth2RedirectUri = new Uri("http://localhost:34106/"); + public const string Id = "bitbucket"; - public static readonly Uri OAuth2AuthorizationEndpoint = new Uri("https://bitbucket.org/site/oauth2/authorize"); - public static readonly Uri OAuth2TokenEndpoint = new Uri("https://bitbucket.org/site/oauth2/access_token"); + public const string Name = "Bitbucket"; - public static class OAuthScopes - { - public const string RepositoryWrite = "repository:write"; - public const string Account = "account"; - } + public const string DefaultAuthenticationHelper = "Atlassian.Bitbucket.UI"; public static class EnvironmentVariables { public const string AuthenticationHelper = "GCM_BITBUCKET_HELPER"; - public const string DevOAuthClientId = "GCM_DEV_BITBUCKET_CLIENTID"; - public const string DevOAuthClientSecret = "GCM_DEV_BITBUCKET_CLIENTSECRET"; - public const string DevOAuthRedirectUri = "GCM_DEV_BITBUCKET_REDIRECTURI"; public const string AuthenticationModes = "GCM_BITBUCKET_AUTHMODES"; public const string AlwaysRefreshCredentials = "GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS"; + public const String ValidateStoredCredentials = "GCM_BITBUCKET_VALIDATE_STORED_CREDENTIALS"; } public static class GitConfiguration @@ -41,14 +23,11 @@ public static class GitConfiguration public static class Credential { public const string AuthenticationHelper = "bitbucketHelper"; - public const string DevOAuthClientId = "bitbucketDevClientId"; - public const string DevOAuthClientSecret = "bitbucketDevClientSecret"; - public const string DevOAuthRedirectUri = "bitbucketDevRedirectUri"; public const string AuthenticationModes = "bitbucketAuthModes"; public const string AlwaysRefreshCredentials = "bitbucketAlwaysRefreshCredentials"; + public const string ValidateStoredCredentials = "bitbucketValidateStoredCredentials"; } } - public static class HelpUrls { public const string DataCenterPasswordReset = "/passwordreset"; @@ -58,14 +37,5 @@ public static class HelpUrls public const string TwoFactor = "https://support.atlassian.com/bitbucket-cloud/docs/enable-two-step-verification/"; } - /// - /// Supported authentication modes for Bitbucket.org - /// - public const AuthenticationModes DotOrgAuthenticationModes = AuthenticationModes.Basic | AuthenticationModes.OAuth; - - /// - /// Supported authentication modes for Bitbucket Server/DC - /// - public const AuthenticationModes ServerAuthenticationModes = AuthenticationModes.Basic; } } diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHelper.cs b/src/shared/Atlassian.Bitbucket/BitbucketHelper.cs new file mode 100644 index 000000000..3624627f0 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/BitbucketHelper.cs @@ -0,0 +1,32 @@ +using System; +using Atlassian.Bitbucket.Cloud; +using GitCredentialManager; + +namespace Atlassian.Bitbucket +{ + public static class BitbucketHelper + { + public static string GetBaseUri(Uri remoteUri) + { + var pathParts = remoteUri.PathAndQuery.Split('/'); + var pathPart = remoteUri.PathAndQuery.StartsWith("/") ? pathParts[1] : pathParts[0]; + var path = !string.IsNullOrWhiteSpace(pathPart) ? "/" + pathPart : null; + return $"{remoteUri.Scheme}://{remoteUri.Host}:{remoteUri.Port}{path}"; + } + + public static bool IsBitbucketOrg(InputArguments input) + { + return IsBitbucketOrg(input.GetRemoteUri()); + } + + public static bool IsBitbucketOrg(Uri targetUri) + { + return IsBitbucketOrg(targetUri.Host); + } + + public static bool IsBitbucketOrg(string targetHost) + { + return StringComparer.OrdinalIgnoreCase.Equals(targetHost, CloudConstants.BitbucketBaseUrlHost); + } + } +} diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs index 0db55bd0d..f1950aca1 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Atlassian.Bitbucket.Cloud; using GitCredentialManager; using GitCredentialManager.Authentication.OAuth; @@ -12,27 +13,27 @@ public class BitbucketHostProvider : IHostProvider { private readonly ICommandContext _context; private readonly IBitbucketAuthentication _bitbucketAuth; - private readonly IBitbucketRestApi _bitbucketApi; + private readonly IRegistry _restApiRegistry; public BitbucketHostProvider(ICommandContext context) - : this(context, new BitbucketAuthentication(context), new BitbucketRestApi(context)) { } + : this(context, new BitbucketAuthentication(context), new BitbucketRestApiRegistry(context)) { } - public BitbucketHostProvider(ICommandContext context, IBitbucketAuthentication bitbucketAuth, IBitbucketRestApi bitbucketApi) + public BitbucketHostProvider(ICommandContext context, IBitbucketAuthentication bitbucketAuth, IRegistry restApiRegistry) { EnsureArgument.NotNull(context, nameof(context)); EnsureArgument.NotNull(bitbucketAuth, nameof(bitbucketAuth)); - EnsureArgument.NotNull(bitbucketApi, nameof(bitbucketApi)); + EnsureArgument.NotNull(restApiRegistry, nameof(restApiRegistry)); _context = context; _bitbucketAuth = bitbucketAuth; - _bitbucketApi = bitbucketApi; + _restApiRegistry = restApiRegistry; } #region IHostProvider - public string Id => "bitbucket"; + public string Id => BitbucketConstants.Id; - public string Name => "Bitbucket"; + public string Name => BitbucketConstants.Name; public IEnumerable SupportedAuthorityIds => BitbucketAuthentication.AuthorityIds; @@ -54,7 +55,7 @@ public bool IsSupported(InputArguments input) // error message for the user in `GetCredentialAsync`. return (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") || StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https")) && - hostName.EndsWith(BitbucketConstants.BitbucketBaseUrlHost, StringComparison.OrdinalIgnoreCase); + hostName.EndsWith(CloudConstants.BitbucketBaseUrlHost, StringComparison.OrdinalIgnoreCase); } public bool IsSupported(HttpResponseMessage response) @@ -76,20 +77,18 @@ public async Task GetCredentialAsync(InputArguments input) { // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") - && input.TryGetHostAndPort(out string host, out _) && IsBitbucketOrg(host)) + && BitbucketHelper.IsBitbucketOrg(input)) { throw new Exception("Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS."); } - Uri remoteUri = input.GetRemoteUri(); - - AuthenticationModes authModes = GetSupportedAuthenticationModes(remoteUri); + var authModes = await GetSupportedAuthenticationModesAsync(input); - return await GetStoredCredentials(remoteUri, input.UserName, authModes) ?? - await GetRefreshedCredentials(remoteUri, input.UserName, authModes); + return await GetStoredCredentials(input, authModes) ?? + await GetRefreshedCredentials(input, authModes); } - private async Task GetStoredCredentials(Uri remoteUri, string userName, AuthenticationModes authModes) + private async Task GetStoredCredentials(InputArguments input, AuthenticationModes authModes) { if (_context.Settings.TryGetSetting(BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.AlwaysRefreshCredentials, @@ -99,10 +98,11 @@ private async Task GetStoredCredentials(Uri remoteUri, string userN return null; } + Uri remoteUri = input.GetRemoteUri(); string credentialService = GetServiceName(remoteUri); _context.Trace.WriteLine($"Look for existing credentials under {credentialService} ..."); - ICredential credentials = _context.CredentialStore.Get(credentialService, userName); + ICredential credentials = _context.CredentialStore.Get(credentialService, input.UserName); if (credentials == null) { @@ -113,7 +113,7 @@ private async Task GetStoredCredentials(Uri remoteUri, string userN _context.Trace.WriteLineSecrets($"Found stored credentials: {credentials.Account}/{{0}}", new object[] { credentials.Password }); // Check credentials are still valid - if (!await ValidateCredentialsWork(remoteUri, credentials, authModes)) + if (!await ValidateCredentialsWork(input, credentials, authModes)) { return null; } @@ -121,16 +121,17 @@ private async Task GetStoredCredentials(Uri remoteUri, string userN return credentials; } - private async Task GetRefreshedCredentials(Uri remoteUri, string userName, AuthenticationModes authModes) + private async Task GetRefreshedCredentials(InputArguments input, AuthenticationModes authModes) { _context.Trace.WriteLine("Refresh credentials..."); // Check for presence of refresh_token entry in credential store + Uri remoteUri = input.GetRemoteUri(); var refreshTokenService = GetRefreshTokenServiceName(remoteUri); _context.Trace.WriteLine("Checking for refresh token..."); ICredential refreshToken = SupportsOAuth(authModes) - ? _context.CredentialStore.Get(refreshTokenService, userName) + ? _context.CredentialStore.Get(refreshTokenService, input.UserName) : null; if (refreshToken is null) @@ -141,7 +142,7 @@ private async Task GetRefreshedCredentials(Uri remoteUri, string us _context.Trace.WriteLine("Prompt for credentials..."); - var result = await _bitbucketAuth.GetCredentialsAsync(remoteUri, userName, authModes); + var result = await _bitbucketAuth.GetCredentialsAsync(remoteUri, input.UserName, authModes); if (result is null || result.AuthenticationMode == AuthenticationModes.None) { _context.Trace.WriteLine("User cancelled credential prompt"); @@ -171,7 +172,7 @@ private async Task GetRefreshedCredentials(Uri remoteUri, string us try { - return await GetOAuthCredentialsViaRefreshFlow(remoteUri, refreshToken); + return await GetOAuthCredentialsViaRefreshFlow(input, refreshToken); } catch (OAuth2Exception ex) { @@ -183,19 +184,21 @@ private async Task GetRefreshedCredentials(Uri remoteUri, string us } } - return await GetOAuthCredentialsViaInteractiveBrowserFlow(remoteUri); + return await GetOAuthCredentialsViaInteractiveBrowserFlow(input); } - private async Task GetOAuthCredentialsViaRefreshFlow(Uri remoteUri, ICredential refreshToken) + private async Task GetOAuthCredentialsViaRefreshFlow(InputArguments input, ICredential refreshToken) { + Uri remoteUri = input.GetRemoteUri(); + var refreshTokenService = GetRefreshTokenServiceName(remoteUri); _context.Trace.WriteLine("Refreshing OAuth credentials using refresh token..."); - OAuth2TokenResult refreshResult = await _bitbucketAuth.RefreshOAuthCredentialsAsync(refreshToken.Password); + OAuth2TokenResult refreshResult = await _bitbucketAuth.RefreshOAuthCredentialsAsync(input, refreshToken.Password); // Resolve the username _context.Trace.WriteLine("Resolving username for refreshed OAuth credential..."); - string refreshUserName = await ResolveOAuthUserNameAsync(refreshResult.AccessToken); + string refreshUserName = await ResolveOAuthUserNameAsync(input, refreshResult.AccessToken); _context.Trace.WriteLine($"Username for refreshed OAuth credential is '{refreshUserName}'"); // Store the refreshed RT @@ -206,8 +209,10 @@ private async Task GetOAuthCredentialsViaRefreshFlow(Uri remoteUri, return new GitCredential(refreshUserName, refreshResult.AccessToken); } - private async Task GetOAuthCredentialsViaInteractiveBrowserFlow(Uri remoteUri) + private async Task GetOAuthCredentialsViaInteractiveBrowserFlow(InputArguments input) { + Uri remoteUri = input.GetRemoteUri(); + var refreshTokenService = GetRefreshTokenServiceName(remoteUri); // We failed to use the refresh token either because it didn't exist, or because the refresh token is no @@ -215,11 +220,11 @@ private async Task GetOAuthCredentialsViaInteractiveBrowserFlow(Uri // Start OAuth authentication flow _context.Trace.WriteLine("Starting OAuth authentication flow..."); - OAuth2TokenResult oauthResult = await _bitbucketAuth.CreateOAuthCredentialsAsync(remoteUri); + OAuth2TokenResult oauthResult = await _bitbucketAuth.CreateOAuthCredentialsAsync(input); // Resolve the username _context.Trace.WriteLine("Resolving username for OAuth credential..."); - string newUserName = await ResolveOAuthUserNameAsync(oauthResult.AccessToken); + string newUserName = await ResolveOAuthUserNameAsync(input, oauthResult.AccessToken); _context.Trace.WriteLine($"Username for OAuth credential is '{newUserName}'"); // Store the new RT @@ -241,14 +246,8 @@ private static bool SupportsBasicAuth(AuthenticationModes authModes) return (authModes & AuthenticationModes.Basic) != 0; } - public AuthenticationModes GetSupportedAuthenticationModes(Uri targetUri) + public async Task GetSupportedAuthenticationModesAsync(InputArguments input) { - if (!IsBitbucketOrg(targetUri)) - { - // Bitbucket Server/DC should use Basic only - return BitbucketConstants.ServerAuthenticationModes; - } - // Check for an explicit override for supported authentication modes if (_context.Settings.TryGetSetting( BitbucketConstants.EnvironmentVariables.AuthenticationModes, @@ -266,10 +265,47 @@ public AuthenticationModes GetSupportedAuthenticationModes(Uri targetUri) } } - // Bitbucket.org should use Basic, OAuth or manual PAT based authentication only - _context.Trace.WriteLine($"{targetUri} is bitbucket.org - authentication schemes: '{BitbucketConstants.DotOrgAuthenticationModes}'"); - return BitbucketConstants.DotOrgAuthenticationModes; + // It isn't possible to detect what Bitbucket.org is expecting so return the predefined answers. + if (BitbucketHelper.IsBitbucketOrg(input)) + { + // Bitbucket should use Basic, OAuth or manual PAT based authentication only + _context.Trace.WriteLine($"{input.GetRemoteUri()} is bitbucket.org - authentication schemes: '{CloudConstants.DotOrgAuthenticationModes}'"); + return CloudConstants.DotOrgAuthenticationModes; + } + + // For Bitbucket DC/Server the supported modes can be detected + _context.Trace.WriteLine($"{input.GetRemoteUri()} is Bitbucket DC - checking for supported authentication schemes..."); + + try + { + var authenticationMethods = await _restApiRegistry.Get(input).GetAuthenticationMethodsAsync(); + + var modes = AuthenticationModes.None; + + if (authenticationMethods.Contains(AuthenticationMethod.BasicAuth)) + { + modes |= AuthenticationModes.Basic; + } + + var isOauthInstalled = await _restApiRegistry.Get(input).IsOAuthInstalledAsync(); + if (isOauthInstalled) + { + modes |= AuthenticationModes.OAuth; + } + + _context.Trace.WriteLine($"Bitbucket DC/Server instance supports authentication schemes: {modes}"); + return modes; + } + catch (Exception ex) + { + _context.Trace.WriteLine($"Failed to query '{input.GetRemoteUri()}' for supported authentication schemes."); + _context.Trace.WriteException(ex); + + _context.Terminal.WriteLine($"warning: failed to query '{input.GetRemoteUri()}' for supported authentication schemes."); + // Fall-back to offering all modes so the user is never blocked from authenticating by at least one mode + return AuthenticationModes.All; + } } public Task StoreCredentialAsync(InputArguments input) @@ -312,9 +348,9 @@ public Task EraseCredentialAsync(InputArguments input) #region Private Methods - private async Task ResolveOAuthUserNameAsync(string accessToken) + private async Task ResolveOAuthUserNameAsync(InputArguments input, string accessToken) { - RestApiResult result = await _bitbucketApi.GetUserInformationAsync(null, accessToken, isBearerToken: true); + RestApiResult result = await _restApiRegistry.Get(input).GetUserInformationAsync(null, accessToken, isBearerToken: true); if (result.Succeeded) { return result.Response.UserName; @@ -323,9 +359,9 @@ private async Task ResolveOAuthUserNameAsync(string accessToken) throw new Exception($"Failed to resolve username. HTTP: {result.StatusCode}"); } - private async Task ResolveBasicAuthUserNameAsync(string username, string password) + private async Task ResolveBasicAuthUserNameAsync(InputArguments input, string username, string password) { - RestApiResult result = await _bitbucketApi.GetUserInformationAsync(username, password, isBearerToken: false); + RestApiResult result = await _restApiRegistry.Get(input).GetUserInformationAsync(username, password, isBearerToken: false); if (result.Succeeded) { return result.Response.UserName; @@ -334,8 +370,17 @@ private async Task ResolveBasicAuthUserNameAsync(string username, string throw new Exception($"Failed to resolve username. HTTP: {result.StatusCode}"); } - private async Task ValidateCredentialsWork(Uri remoteUri, ICredential credentials, AuthenticationModes authModes) + private async Task ValidateCredentialsWork(InputArguments input, ICredential credentials, AuthenticationModes authModes) { + if (_context.Settings.TryGetSetting( + BitbucketConstants.EnvironmentVariables.ValidateStoredCredentials, + Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials, + out string validateStoredCredentials) && !validateStoredCredentials.ToBooleanyOrDefault(true)) + { + _context.Trace.WriteLine($"Skipping validation of stored credentials due to {BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials} = {validateStoredCredentials}"); + return true; + } + if (credentials is null) { return false; @@ -344,31 +389,23 @@ private async Task ValidateCredentialsWork(Uri remoteUri, ICredential cred // TODO: ideally we'd also check if the credentials have expired based on some local metadata // (once/if we get such metadata storage), and return false if they have. // This would be more efficient than having to make REST API calls to check. - + Uri remoteUri = input.GetRemoteUri(); _context.Trace.WriteLineSecrets($"Validate credentials ({credentials.Account}/{{0}}) are fresh for {remoteUri} ...", new object[] { credentials.Password }); - if (!IsBitbucketOrg(remoteUri)) - { - // TODO: Validate DC/Server credentials before returning them to Git - // Currently credentials for DC/Server are not checked by GCM. - // Instead the validation relies on Git to try and fail with the credentials and then request GCM to erase them - _context.Trace.WriteLine("For DC/Server skip validating existing credentials"); - return await Task.FromResult(true); - } - // Bitbucket supports both OAuth + Basic Auth unless there is explicit GCM configuration. // The credentials could be for either scheme therefore need to potentially test both possibilities. if (SupportsOAuth(authModes)) { try { - await ResolveOAuthUserNameAsync(credentials.Password); + await ResolveOAuthUserNameAsync(input, credentials.Password); _context.Trace.WriteLine("Validated existing credentials using OAuth"); return true; } - catch (Exception) + catch (Exception ex) { - _context.Trace.WriteLine("Failed to validate existing credentials using OAuth"); + _context.Trace.WriteLine($"Failed to validate existing credentials using OAuth"); + _context.Trace.WriteException(ex); } } @@ -376,13 +413,14 @@ private async Task ValidateCredentialsWork(Uri remoteUri, ICredential cred { try { - await ResolveBasicAuthUserNameAsync(credentials.Account, credentials.Password); + await ResolveBasicAuthUserNameAsync(input, credentials.Account, credentials.Password); _context.Trace.WriteLine("Validated existing credentials using BasicAuth"); return true; } - catch (Exception) + catch (Exception ex) { - _context.Trace.WriteLine("Failed to validate existing credentials using Basic Auth"); + _context.Trace.WriteLine($"Failed to validate existing credentials using Basic Auth"); + _context.Trace.WriteException(ex); return false; } } @@ -406,21 +444,11 @@ private static string GetServiceName(Uri remoteUri) return uri.AbsoluteUri.TrimEnd('/'); } - public static bool IsBitbucketOrg(Uri targetUri) - { - return IsBitbucketOrg(targetUri.Host); - } - - public static bool IsBitbucketOrg(string host) - { - return StringComparer.OrdinalIgnoreCase.Equals(host, BitbucketConstants.BitbucketBaseUrlHost); - } - #endregion public void Dispose() { - _bitbucketApi.Dispose(); + _restApiRegistry.Dispose(); _bitbucketAuth.Dispose(); } } diff --git a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs index cda6ff2f2..9d4f0043e 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs @@ -1,64 +1,35 @@ -using System; +using System; +using System.Collections.Generic; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using GitCredentialManager; using GitCredentialManager.Authentication.OAuth; -using GitCredentialManager.Authentication.OAuth.Json; -using Newtonsoft.Json; namespace Atlassian.Bitbucket { - public class BitbucketOAuth2Client : OAuth2Client + public abstract class BitbucketOAuth2Client : OAuth2Client { - private static readonly OAuth2ServerEndpoints Endpoints = new OAuth2ServerEndpoints( - BitbucketConstants.OAuth2AuthorizationEndpoint, - BitbucketConstants.OAuth2TokenEndpoint); - - public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings) - : base(httpClient, Endpoints, - GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings)) + public BitbucketOAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri, string clientSecret, ITrace trace) : base(httpClient, endpoints, clientId, redirectUri, clientSecret, trace, false) { } - private static string GetClientId(ISettings settings) - { - // Check for developer override value - if (settings.TryGetSetting( - BitbucketConstants.EnvironmentVariables.DevOAuthClientId, - Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.DevOAuthClientId, - out string clientId)) - { - return clientId; - } - - return BitbucketConstants.OAuth2ClientId; - } + public abstract IEnumerable Scopes { get; } - private static Uri GetRedirectUri(ISettings settings) + public string GetRefreshTokenServiceName(InputArguments input) { - // Check for developer override value - if (settings.TryGetSetting( - BitbucketConstants.EnvironmentVariables.DevOAuthRedirectUri, - Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.DevOAuthRedirectUri, - out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri)) - { - return redirectUri; - } + Uri baseUri = input.GetRemoteUri(includeUser: false); - return BitbucketConstants.OAuth2RedirectUri; + // The refresh token key never includes the path component. + // Instead we use the path component to specify this is the "refresh_token". + Uri uri = new UriBuilder(baseUri) { Path = "/refresh_token" }.Uri; + + return uri.AbsoluteUri.TrimEnd('/'); } - private static string GetClientSecret(ISettings settings) + public Task GetAuthorizationCodeAsync(IOAuth2WebBrowser browser, CancellationToken ct) { - // Check for developer override value - if (settings.TryGetSetting( - BitbucketConstants.EnvironmentVariables.DevOAuthClientSecret, - Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.DevOAuthClientSecret, - out string clientId)) - { - return clientId; - } - - return BitbucketConstants.OAuth2ClientSecret; + return GetAuthorizationCodeAsync(Scopes, browser, ct); } protected override bool TryCreateTokenEndpointResult(string json, out OAuth2TokenResult result) @@ -75,12 +46,5 @@ protected override bool TryCreateTokenEndpointResult(string json, out OAuth2Toke result = null; return false; } - - private class BitbucketTokenEndpointResponseJson : TokenEndpointResponseJson - { - // Bitbucket uses "scopes" for the scopes property name rather than the standard "scope" name - [JsonProperty("scopes")] - public override string Scope { get; set; } - } } } diff --git a/src/shared/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs b/src/shared/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs new file mode 100644 index 000000000..950a46855 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs @@ -0,0 +1,37 @@ +using Atlassian.Bitbucket.Cloud; +using GitCredentialManager; + +namespace Atlassian.Bitbucket +{ + public class BitbucketRestApiRegistry : IRegistry + { + private readonly ICommandContext context; + private BitbucketRestApi cloudApi; + private DataCenter.BitbucketRestApi dataCenterApi; + + public BitbucketRestApiRegistry(ICommandContext context) + { + this.context = context; + } + + public IBitbucketRestApi Get(InputArguments input) + { + if(!BitbucketHelper.IsBitbucketOrg(input)) + { + return DataCenterApi; + } + + return CloudApi; + } + + public void Dispose() + { + context.Dispose(); + cloudApi?.Dispose(); + dataCenterApi?.Dispose(); + } + + private Cloud.BitbucketRestApi CloudApi => cloudApi ??= new Cloud.BitbucketRestApi(context); + private DataCenter.BitbucketRestApi DataCenterApi => dataCenterApi ??= new DataCenter.BitbucketRestApi(context); + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs b/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs new file mode 100644 index 000000000..7468b09c4 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs @@ -0,0 +1,12 @@ +using GitCredentialManager.Authentication.OAuth.Json; +using Newtonsoft.Json; + +namespace Atlassian.Bitbucket +{ + public class BitbucketTokenEndpointResponseJson : TokenEndpointResponseJson + { + // Bitbucket uses "scopes" for the scopes property name rather than the standard "scope" name + [JsonProperty("scopes")] + public override string Scope { get; set; } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs new file mode 100644 index 000000000..9e49c10c6 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +using System; +using System.Collections.Generic; +using System.Net.Http; +using GitCredentialManager; +using GitCredentialManager.Authentication.OAuth; + +namespace Atlassian.Bitbucket.Cloud +{ + public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client + { + public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace trace) + : base(httpClient, GetEndpoints(), + GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace) + { + } + + public override IEnumerable Scopes => new string[] { + CloudConstants.OAuthScopes.RepositoryWrite, + CloudConstants.OAuthScopes.Account, + }; + + private static string GetClientId(ISettings settings) + { + // Check for developer override value + if (settings.TryGetSetting( + CloudConstants.EnvironmentVariables.OAuthClientId, + Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientId, + out string clientId)) + { + return clientId; + } + + return CloudConstants.OAuth2ClientId; + } + + private static Uri GetRedirectUri(ISettings settings) + { + // Check for developer override value + if (settings.TryGetSetting( + CloudConstants.EnvironmentVariables.OAuthRedirectUri, + Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthRedirectUri, + out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri)) + { + return redirectUri; + } + + return CloudConstants.OAuth2RedirectUri; + } + + private static string GetClientSecret(ISettings settings) + { + // Check for developer override value + if (settings.TryGetSetting( + CloudConstants.EnvironmentVariables.OAuthClientSecret, + Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientSecret, + out string clientSecret)) + { + return clientSecret; + } + + return CloudConstants.OAuth2ClientSecret; + } + + private static OAuth2ServerEndpoints GetEndpoints() + { + return new OAuth2ServerEndpoints( + CloudConstants.OAuth2AuthorizationEndpoint, + CloudConstants.OAuth2TokenEndpoint + ); + } + } +} diff --git a/src/shared/Atlassian.Bitbucket/BitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs similarity index 55% rename from src/shared/Atlassian.Bitbucket/BitbucketRestApi.cs rename to src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs index 21b658fb5..ec56436be 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketRestApi.cs +++ b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs @@ -1,54 +1,16 @@ using System; -using System.Net; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using GitCredentialManager; using Newtonsoft.Json; -namespace Atlassian.Bitbucket +namespace Atlassian.Bitbucket.Cloud { - public interface IBitbucketRestApi : IDisposable - { - Task> GetUserInformationAsync(string userName, string password, bool isBearerToken); - } - - public class RestApiResult - { - public RestApiResult(HttpStatusCode statusCode) - : this(statusCode, default(T)) { } - - public RestApiResult(HttpStatusCode statusCode, T response) - { - StatusCode = statusCode; - Response = response; - } - - public HttpStatusCode StatusCode { get; } - - public T Response { get; } - - public bool Succeeded => 199 < (int) StatusCode && (int) StatusCode < 300; - } - - public class UserInfo - { - [JsonProperty("has_2fa_enabled")] - public bool IsTwoFactorAuthenticationEnabled { get; set; } - - [JsonProperty("username")] - public string UserName { get; set; } - - [JsonProperty("account_id")] - public string AccountId { get; set; } - - [JsonProperty("uuid")] - public Guid Uuid { get; set; } - } - public class BitbucketRestApi : IBitbucketRestApi { private readonly ICommandContext _context; - private readonly Uri _apiUri = BitbucketConstants.BitbucketApiUri; + private readonly Uri _apiUri = CloudConstants.BitbucketApiUri; public BitbucketRestApi(ICommandContext context) { @@ -57,7 +19,7 @@ public BitbucketRestApi(ICommandContext context) _context = context; } - public async Task> GetUserInformationAsync(string userName, string password, bool isBearerToken) + public async Task> GetUserInformationAsync(string userName, string password, bool isBearerToken) { var requestUri = new Uri(_apiUri, "2.0/user"); using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri)) @@ -82,14 +44,27 @@ public async Task> GetUserInformationAsync(string userNa { var obj = JsonConvert.DeserializeObject(json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - return new RestApiResult(response.StatusCode, obj); + return new RestApiResult(response.StatusCode, obj); } - return new RestApiResult(response.StatusCode); + return new RestApiResult(response.StatusCode); } } } + public Task IsOAuthInstalledAsync() + { + return Task.FromResult(true); + } + + public Task> GetAuthenticationMethodsAsync() + { + // For Bitbucket Cloud there is no REST API to determine login methods + // instead this is determined later in the process by attempting + // authenticated REST API requests and checking the response. + return Task.FromResult(new List()); + } + private HttpClient _httpClient; private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient(); diff --git a/src/shared/Atlassian.Bitbucket/Cloud/CloudConstants.cs b/src/shared/Atlassian.Bitbucket/Cloud/CloudConstants.cs new file mode 100644 index 000000000..574435872 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/Cloud/CloudConstants.cs @@ -0,0 +1,50 @@ +using System; + +namespace Atlassian.Bitbucket.Cloud +{ + public static class CloudConstants + { + public const string BitbucketBaseUrlHost = "bitbucket.org"; + public static readonly Uri BitbucketApiUri = new Uri("https://api.bitbucket.org"); + + // TODO: use the GCM client ID and secret once we have this approved. + // Until then continue to use Sourcetree's values like GCM Windows. + //public const string OAuth2ClientId = "b5AKdPfpgFdEGpKzPE"; + //public const string OAuth2ClientSecret = "7NUP5qUtSR3SxdFK4xAGaU6PMNvNdE59"; + //public static readonly Uri OAuth2RedirectUri = new Uri("http://localhost:46337/"); + public const string OAuth2ClientId = "HJdmKXV87DsmC9zSWB"; + public const string OAuth2ClientSecret = "wwWw47VB9ZHwMsD4Q4rAveHkbxNrMp3n"; + public static readonly Uri OAuth2RedirectUri = new Uri("http://localhost:34106/"); + + public static readonly Uri OAuth2AuthorizationEndpoint = new Uri("https://bitbucket.org/site/oauth2/authorize"); + public static readonly Uri OAuth2TokenEndpoint = new Uri("https://bitbucket.org/site/oauth2/access_token"); + + public static class OAuthScopes + { + public const string RepositoryWrite = "repository:write"; + public const string Account = "account"; + } + + /// + /// Supported authentication modes for Bitbucket.org + /// + public const AuthenticationModes DotOrgAuthenticationModes = AuthenticationModes.Basic | AuthenticationModes.OAuth; + + public static class EnvironmentVariables + { + public const string OAuthClientId = "GCM_BITBUCKET_CLOUD_CLIENTID"; + public const string OAuthClientSecret = "GCM_BITBUCKET_CLOUD_CLIENTSECRET"; + public const string OAuthRedirectUri = "GCM_BITBUCKET_CLOUD_OAUTH_REDIRECTURI"; + } + + public static class GitConfiguration + { + public static class Credential + { + public const string OAuthClientId = "cloudOAuthClientId"; + public const string OAuthClientSecret = "cloudOAuthClientSecret"; + public const string OAuthRedirectUri = "cloudOauthRedirectUri"; + } + } + } +} diff --git a/src/shared/Atlassian.Bitbucket/Cloud/UserInfo.cs b/src/shared/Atlassian.Bitbucket/Cloud/UserInfo.cs new file mode 100644 index 000000000..1ecabe6b5 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/Cloud/UserInfo.cs @@ -0,0 +1,20 @@ +using System; +using Newtonsoft.Json; + +namespace Atlassian.Bitbucket.Cloud +{ + public class UserInfo : IUserInfo + { + [JsonProperty("has_2fa_enabled")] + public bool IsTwoFactorAuthenticationEnabled { get; set; } + + [JsonProperty("username")] + public string UserName { get; set; } + + [JsonProperty("account_id")] + public string AccountId { get; set; } + + [JsonProperty("uuid")] + public Guid Uuid { get; set; } + } +} diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs new file mode 100644 index 000000000..378a30f94 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using GitCredentialManager; +using GitCredentialManager.Authentication.OAuth; + +namespace Atlassian.Bitbucket.DataCenter +{ + public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client + { + public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace trace) + : base(httpClient, GetEndpoints(settings), + GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace) + { + } + + public override IEnumerable Scopes => new string[] { + DataCenterConstants.OAuthScopes.PublicRepos, + DataCenterConstants.OAuthScopes.RepoRead, + DataCenterConstants.OAuthScopes.RepoWrite + }; + + private static string GetClientId(ISettings settings) + { + // Check for developer override value + if (settings.TryGetSetting( + DataCenterConstants.EnvironmentVariables.OAuthClientId, + Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientId, + out string clientId)) + { + return clientId; + } + + throw new ArgumentException("Bitbucket DC OAuth Client ID must be defined"); + } + + private static Uri GetRedirectUri(ISettings settings) + { + // Check for developer override value + if (settings.TryGetSetting( + DataCenterConstants.EnvironmentVariables.OAuthRedirectUri, + Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri, + out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri)) + { + return redirectUri; + } + + return DataCenterConstants.OAuth2RedirectUri; + } + + private static string GetClientSecret(ISettings settings) + { + // Check for developer override value + if (settings.TryGetSetting( + DataCenterConstants.EnvironmentVariables.OAuthClientSecret, + Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret, + out string clientSecret)) + { + return clientSecret; + } + + throw new ArgumentException("Bitbucket DC OAuth Client Secret must be defined"); + } + + private static OAuth2ServerEndpoints GetEndpoints(ISettings settings) + { + var remoteUri = settings.RemoteUri; + if (remoteUri == null) + { + throw new ArgumentException("RemoteUri must be defined to generate Bitbucket DC OAuth2 endpoint Urls"); + } + + return new OAuth2ServerEndpoints( + new Uri(BitbucketHelper.GetBaseUri(remoteUri) + "/rest/oauth2/latest/authorize"), + new Uri(BitbucketHelper.GetBaseUri(remoteUri) + "/rest/oauth2/latest/token") + ); + } + } +} diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs new file mode 100644 index 000000000..0688e0323 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using GitCredentialManager; +using Newtonsoft.Json; + +namespace Atlassian.Bitbucket.DataCenter +{ + public class BitbucketRestApi : IBitbucketRestApi + { + private readonly ICommandContext _context; + + private HttpClient _httpClient; + + public BitbucketRestApi(ICommandContext context) + { + EnsureArgument.NotNull(context, nameof(context)); + + _context = context; + + } + + public async Task> GetUserInformationAsync(string userName, string password, bool isBearerToken) + { + if (_context.Settings.TryGetSetting( + BitbucketConstants.EnvironmentVariables.ValidateStoredCredentials, + Constants.GitConfiguration.Credential.SectionName, BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials, + out string validateStoredCredentials) && !validateStoredCredentials.ToBooleanyOrDefault(true)) + { + _context.Trace.WriteLine($"Skipping retreival of user information due to {BitbucketConstants.GitConfiguration.Credential.ValidateStoredCredentials} = {validateStoredCredentials}"); + return new RestApiResult(HttpStatusCode.OK, new UserInfo() { UserName = DataCenterConstants.OAuthUserName });; + } + + // Bitbucket Server/DC doesn't actually provide a REST API we can use to trade an access_token for the owning username, + // therefore this is always going to return a placeholder username, however this call does provide a way to validate the + // credentials we do have + var requestUri = new Uri(ApiUri, "api/1.0/users"); + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri)) + { + if (isBearerToken) + { + request.AddBearerAuthenticationHeader(password); + } + else + { + request.AddBasicAuthenticationHeader(userName, password); + } + + _context.Trace.WriteLine($"HTTP: GET {requestUri}"); + using (HttpResponseMessage response = await HttpClient.SendAsync(request)) + { + _context.Trace.WriteLine($"HTTP: Response {(int) response.StatusCode} [{response.StatusCode}]"); + + string json = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + // No REST API in BBS that can be used to return just my user account based on my login AFAIK. + // but we can prove the credentials work. + return new RestApiResult(HttpStatusCode.OK, new UserInfo() { UserName = DataCenterConstants.OAuthUserName }); + } + + return new RestApiResult(response.StatusCode); + } + } + + } + + public async Task IsOAuthInstalledAsync() + { + var requestUri = new Uri(ApiUri.AbsoluteUri + "oauth2/1.0/client"); + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri)) + { + _context.Trace.WriteLine($"HTTP: GET {requestUri}"); + using (HttpResponseMessage response = await HttpClient.SendAsync(request)) + { + _context.Trace.WriteLine($"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]"); + + if (HttpStatusCode.Unauthorized == response.StatusCode) + { + // accessed anonymously so no access but it does exist. + return true; + } + + return false; + } + } + } + + public async Task> GetAuthenticationMethodsAsync() + { + var authenticationMethods = new List(); + + var requestUri = new Uri(ApiUri.AbsoluteUri + "authconfig/1.0/login-options"); + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri)) + { + _context.Trace.WriteLine($"HTTP: GET {requestUri}"); + using (HttpResponseMessage response = await HttpClient.SendAsync(request)) + { + _context.Trace.WriteLine($"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]"); + + string json = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var loginOptions = JsonConvert.DeserializeObject(json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + + if (loginOptions.Results.Any(r => "LOGIN_FORM".Equals(r.Type))) + { + authenticationMethods.Add(AuthenticationMethod.BasicAuth); + } + + if (loginOptions.Results.Any(r => "IDP".Equals(r.Type))) + { + authenticationMethods.Add(AuthenticationMethod.Sso); + } + } + } + } + + return authenticationMethods; + } + + public void Dispose() + { + _httpClient?.Dispose(); + } + + private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient(); + + private Uri ApiUri + { + get + { + var remoteUri = _context.Settings?.RemoteUri; + if (remoteUri == null) + { + throw new ArgumentException("RemoteUri must be defined to generate Bitbucket DC OAuth2 endpoint Urls"); + } + + return new Uri(BitbucketHelper.GetBaseUri(remoteUri) + "/rest/"); + } + } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs b/src/shared/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs new file mode 100644 index 000000000..526db40c0 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs @@ -0,0 +1,45 @@ +using System; + +namespace Atlassian.Bitbucket.DataCenter +{ + public static class DataCenterConstants + { + public static class OAuthScopes + { + public const string PublicRepos = "PUBLIC_REPOS"; + public const string RepoWrite = "REPO_WRITE"; + public const string RepoRead = "REPO_READ"; + } + + public static readonly Uri OAuth2RedirectUri = new Uri("http://localhost:34106/"); + + /// + /// Supported authentication modes for Bitbucket Server/DC + /// + public const AuthenticationModes ServerAuthenticationModes = AuthenticationModes.Basic | AuthenticationModes.OAuth; + + /// + /// Bitbucket Server/DC does not have a REST API we can use to trade an OAuth access_token for the owning username. + /// However one is needed to construct the Basic Auth request made by Git HTTP requests, therefore use a hardcoded + /// placeholder for the username. + /// + public const string OAuthUserName = "OAUTH_USERNAME"; + + public static class EnvironmentVariables + { + public const string OAuthClientId = "GCM_BITBUCKET_DATACENTER_CLIENTID"; + public const string OAuthClientSecret = "GCM_BITBUCKET_DATACENTER_CLIENTSECRET"; + public const string OAuthRedirectUri = "GCM_BITBUCKET_DATACENTER_OAUTH_REDIRECTURI"; + } + + public static class GitConfiguration + { + public static class Credential + { + public const string OAuthClientId = "bitbucketDataCenterOAuthClientId"; + public const string OAuthClientSecret = "bitbucketDataCenterOAuthClientSecret"; + public const string OAuthRedirectUri = "bitbucketDataCenterOauthRedirectUri"; + } + } + } +} diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/LoginOption.cs b/src/shared/Atlassian.Bitbucket/DataCenter/LoginOption.cs new file mode 100644 index 000000000..73f42a209 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/DataCenter/LoginOption.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Atlassian.Bitbucket.DataCenter +{ + public class LoginOption + { + [JsonProperty("type")] + public string Type { get ; set; } + + [JsonProperty("id")] + public int Id { get; set; } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/LoginOptions.cs b/src/shared/Atlassian.Bitbucket/DataCenter/LoginOptions.cs new file mode 100644 index 000000000..8e631cbf5 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/DataCenter/LoginOptions.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Atlassian.Bitbucket.DataCenter +{ + public class LoginOptions + { + [JsonProperty("results")] + public List Results { get; set; } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/UserInfo.cs b/src/shared/Atlassian.Bitbucket/DataCenter/UserInfo.cs new file mode 100644 index 000000000..298f86f26 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/DataCenter/UserInfo.cs @@ -0,0 +1,12 @@ +using System; + +namespace Atlassian.Bitbucket.DataCenter +{ + public class UserInfo : IUserInfo + { + // Bitbucket DC does not support this property per-user + public bool IsTwoFactorAuthenticationEnabled { get => false; } + + public string UserName { get; set; } + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/IBitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/IBitbucketRestApi.cs new file mode 100644 index 000000000..d89d174b2 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/IBitbucketRestApi.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Atlassian.Bitbucket +{ + public interface IBitbucketRestApi : IDisposable + { + Task> GetUserInformationAsync(string userName, string password, bool isBearerToken); + Task IsOAuthInstalledAsync(); + Task> GetAuthenticationMethodsAsync(); + } +} diff --git a/src/shared/Atlassian.Bitbucket/IRegistry.cs b/src/shared/Atlassian.Bitbucket/IRegistry.cs new file mode 100644 index 000000000..5712e3770 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/IRegistry.cs @@ -0,0 +1,10 @@ +using System; +using GitCredentialManager; + +namespace Atlassian.Bitbucket +{ + public interface IRegistry : IDisposable + { + T Get(InputArguments input); + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/IUserInfo.cs b/src/shared/Atlassian.Bitbucket/IUserInfo.cs new file mode 100644 index 000000000..a496456bd --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/IUserInfo.cs @@ -0,0 +1,9 @@ +using System; +namespace Atlassian.Bitbucket +{ + public interface IUserInfo + { + string UserName{ get; } + bool IsTwoFactorAuthenticationEnabled { get; } + } +} diff --git a/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs b/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs new file mode 100644 index 000000000..573140eb3 --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs @@ -0,0 +1,42 @@ +using System.Net.Http; +using GitCredentialManager; + +namespace Atlassian.Bitbucket +{ + public class OAuth2ClientRegistry : IRegistry + { + private readonly HttpClient http; + private ISettings settings; + private readonly ITrace trace; + private Cloud.BitbucketOAuth2Client cloudClient; + private DataCenter.BitbucketOAuth2Client dataCenterClient; + + public OAuth2ClientRegistry(ICommandContext context) + { + this.http = context.HttpClientFactory.CreateClient(); + this.settings = context.Settings; + this.trace = context.Trace; + } + + public BitbucketOAuth2Client Get(InputArguments input) + { + if (!BitbucketHelper.IsBitbucketOrg(input)) + { + return DataCenterClient; + } + + return CloudClient; + } + + public void Dispose() + { + http.Dispose(); + settings.Dispose(); + cloudClient = null; + dataCenterClient = null; + } + + private Cloud.BitbucketOAuth2Client CloudClient => cloudClient ??= new Cloud.BitbucketOAuth2Client(http, settings, trace); + private DataCenter.BitbucketOAuth2Client DataCenterClient => dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(http, settings, trace); + } +} \ No newline at end of file diff --git a/src/shared/Atlassian.Bitbucket/RestApiResult.cs b/src/shared/Atlassian.Bitbucket/RestApiResult.cs new file mode 100644 index 000000000..63c50cfdd --- /dev/null +++ b/src/shared/Atlassian.Bitbucket/RestApiResult.cs @@ -0,0 +1,22 @@ +using System.Net; + +namespace Atlassian.Bitbucket +{ + public class RestApiResult + { + public RestApiResult(HttpStatusCode statusCode) + : this(statusCode, default(T)) { } + + public RestApiResult(HttpStatusCode statusCode, T response) + { + StatusCode = statusCode; + Response = response; + } + + public HttpStatusCode StatusCode { get; } + + public T Response { get; } + + public bool Succeeded => 199 < (int)StatusCode && (int)StatusCode < 300; + } +} diff --git a/src/shared/Core.Tests/ApplicationTests.cs b/src/shared/Core.Tests/ApplicationTests.cs index e4d0b73cc..d62782f10 100644 --- a/src/shared/Core.Tests/ApplicationTests.cs +++ b/src/shared/Core.Tests/ApplicationTests.cs @@ -11,7 +11,7 @@ public class ApplicationTests public async Task Application_ConfigureAsync_NoHelpers_AddsEmptyAndGcm() { const string emptyHelper = ""; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -29,7 +29,7 @@ public async Task Application_ConfigureAsync_NoHelpers_AddsEmptyAndGcm() public async Task Application_ConfigureAsync_Gcm_AddsEmptyBeforeGcm() { const string emptyHelper = ""; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -50,7 +50,7 @@ public async Task Application_ConfigureAsync_Gcm_AddsEmptyBeforeGcm() public async Task Application_ConfigureAsync_EmptyAndGcm_DoesNothing() { const string emptyHelper = ""; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -75,7 +75,7 @@ public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersBefore_DoesNot { const string emptyHelper = ""; const string beforeHelper = "foo"; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -101,7 +101,7 @@ public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersAfter_DoesNoth { const string emptyHelper = ""; const string afterHelper = "foo"; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -128,7 +128,7 @@ public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersBeforeAndAfter const string emptyHelper = ""; const string beforeHelper = "foo"; const string afterHelper = "bar"; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -155,7 +155,7 @@ public async Task Application_ConfigureAsync_EmptyAndGcmWithEmptyAfter_RemovesEx { const string emptyHelper = ""; const string afterHelper = "foo"; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -181,7 +181,7 @@ public async Task Application_ConfigureAsync_EmptyAndGcmWithEmptyAfter_RemovesEx [Fact] public async Task Application_UnconfigureAsync_NoHelpers_DoesNothing() { - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -194,7 +194,7 @@ public async Task Application_UnconfigureAsync_NoHelpers_DoesNothing() [Fact] public async Task Application_UnconfigureAsync_Gcm_RemovesGcm() { - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -211,7 +211,7 @@ public async Task Application_UnconfigureAsync_Gcm_RemovesGcm() public async Task Application_UnconfigureAsync_EmptyAndGcm_RemovesEmptyAndGcm() { const string emptyHelper = ""; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -229,7 +229,7 @@ public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersBefore_Remov { const string emptyHelper = ""; const string beforeHelper = "foo"; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -253,7 +253,7 @@ public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersAfterBefore_ { const string emptyHelper = ""; const string afterHelper = "bar"; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; @@ -279,7 +279,7 @@ public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersBeforeAndAft const string emptyHelper = ""; const string beforeHelper = "foo"; const string afterHelper = "bar"; - const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; var context = new TestCommandContext {AppPath = executablePath}; diff --git a/src/shared/Core.Tests/Authentication/BasicAuthenticationTests.cs b/src/shared/Core.Tests/Authentication/BasicAuthenticationTests.cs index a991108f4..7539a37c1 100644 --- a/src/shared/Core.Tests/Authentication/BasicAuthenticationTests.cs +++ b/src/shared/Core.Tests/Authentication/BasicAuthenticationTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; using GitCredentialManager.Authentication; using GitCredentialManager.Tests.Objects; using Moq; @@ -14,11 +16,11 @@ public void BasicAuthentication_GetCredentials_NullResource_ThrowsException() var context = new TestCommandContext(); var basicAuth = new BasicAuthentication(context); - Assert.Throws(() => basicAuth.GetCredentials(null)); + Assert.ThrowsAsync(() => basicAuth.GetCredentialsAsync(null)); } [Fact] - public void BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUserName_PasswordPromptReturnsCredentials() + public async Task BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUserName_PasswordPromptReturnsCredentials() { const string testResource = "https://example.com"; const string testUserName = "john.doe"; @@ -29,14 +31,14 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUser var basicAuth = new BasicAuthentication(context); - ICredential credential = basicAuth.GetCredentials(testResource, testUserName); + ICredential credential = await basicAuth.GetCredentialsAsync(testResource, testUserName); Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); } [Fact] - public void BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPassPromptReturnsCredentials() + public async Task BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPassPromptReturnsCredentials() { const string testResource = "https://example.com"; const string testUserName = "john.doe"; @@ -48,7 +50,7 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPa var basicAuth = new BasicAuthentication(context); - ICredential credential = basicAuth.GetCredentials(testResource); + ICredential credential = await basicAuth.GetCredentialsAsync(testResource); Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); @@ -67,11 +69,11 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_NoTerminalPromp var basicAuth = new BasicAuthentication(context); - Assert.Throws(() => basicAuth.GetCredentials(testResource)); + Assert.ThrowsAsync(() => basicAuth.GetCredentialsAsync(testResource)); } - [PlatformFact(Platforms.Windows)] - public void BasicAuthentication_GetCredentials_DesktopSession_Resource_UserPassPromptReturnsCredentials() + [Fact] + public async Task BasicAuthentication_GetCredentials_DesktopSession_CallsHelper() { const string testResource = "https://example.com"; const string testUserName = "john.doe"; @@ -79,88 +81,66 @@ public void BasicAuthentication_GetCredentials_DesktopSession_Resource_UserPassP var context = new TestCommandContext { - SessionManager = {IsDesktopSession = true}, - SystemPrompts = - { - CredentialPrompt = (resource, userName) => - { - Assert.Equal(testResource, resource); - Assert.Null(userName); - - return new GitCredential(testUserName, testPassword); - } - } + SessionManager = {IsDesktopSession = true} }; - var basicAuth = new BasicAuthentication(context); - - ICredential credential = basicAuth.GetCredentials(testResource); - - Assert.NotNull(credential); - Assert.Equal(testUserName, credential.Account); - Assert.Equal(testPassword, credential.Password); - } - - [PlatformFact(Platforms.Windows)] - public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptReturnsCredentials() - { - const string testResource = "https://example.com"; - const string testUserName = "john.doe"; - const string testPassword = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")] - - var context = new TestCommandContext - { - SessionManager = {IsDesktopSession = true}, - SystemPrompts = - { - CredentialPrompt = (resource, userName) => + context.FileSystem.Files["/usr/local/bin/git-credential-manager-ui"] = new byte[0]; + context.FileSystem.Files[@"C:\Program Files\Git Credential Manager Core\git-credential-manager-ui.exe"] = new byte[0]; + + var auth = new Mock(MockBehavior.Strict, context); + auth.Setup(x => x.InvokeHelperAsync( + It.IsAny(), + $"basic --resource {testResource}", + It.IsAny>(), + It.IsAny())) + .ReturnsAsync( + new Dictionary { - Assert.Equal(testResource, resource); - Assert.Equal(testUserName, userName); - - return new GitCredential(testUserName, testPassword); + ["username"] = testUserName, + ["password"] = testPassword } - } - }; - - var basicAuth = new BasicAuthentication(context); + ); - ICredential credential = basicAuth.GetCredentials(testResource, testUserName); + ICredential credential = await auth.Object.GetCredentialsAsync(testResource); Assert.NotNull(credential); Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); } - [PlatformFact(Platforms.Windows)] - public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptDiffUserReturnsCredentials() + [Fact] + public async Task BasicAuthentication_GetCredentials_DesktopSession_UserName_CallsHelper() { const string testResource = "https://example.com"; const string testUserName = "john.doe"; - const string newUserName = "jane.doe"; const string testPassword = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")] var context = new TestCommandContext { - SessionManager = {IsDesktopSession = true}, - SystemPrompts = - { - CredentialPrompt = (resource, userName) => - { - Assert.Equal(testResource, resource); - Assert.Equal(testUserName, userName); - - return new GitCredential(newUserName, testPassword); - } - } + SessionManager = {IsDesktopSession = true} }; - var basicAuth = new BasicAuthentication(context); + context.FileSystem.Files["/usr/local/bin/git-credential-manager-ui"] = new byte[0]; + context.FileSystem.Files[@"C:\Program Files\Git Credential Manager Core\git-credential-manager-ui.exe"] = new byte[0]; + + var auth = new Mock(MockBehavior.Strict, context); + auth.Setup(x => x.InvokeHelperAsync( + It.IsAny(), + $"basic --resource {testResource} --username {testUserName}", + It.IsAny>(), + It.IsAny())) + .ReturnsAsync( + new Dictionary + { + ["username"] = testUserName, + ["password"] = testPassword + } + ); - ICredential credential = basicAuth.GetCredentials(testResource, testUserName); + ICredential credential = await auth.Object.GetCredentialsAsync(testResource, testUserName); Assert.NotNull(credential); - Assert.Equal(newUserName, credential.Account); + Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); } } diff --git a/src/shared/Core.Tests/Core.Tests.csproj b/src/shared/Core.Tests/Core.Tests.csproj index 0bc07acd2..da473f014 100644 --- a/src/shared/Core.Tests/Core.Tests.csproj +++ b/src/shared/Core.Tests/Core.Tests.csproj @@ -14,7 +14,7 @@ all - + diff --git a/src/shared/Core.Tests/GenericHostProviderTests.cs b/src/shared/Core.Tests/GenericHostProviderTests.cs index d118ff57e..42dc62177 100644 --- a/src/shared/Core.Tests/GenericHostProviderTests.cs +++ b/src/shared/Core.Tests/GenericHostProviderTests.cs @@ -83,8 +83,8 @@ public async Task GenericHostProvider_CreateCredentialAsync_WiaNotAllowed_Return Settings = {IsWindowsIntegratedAuthenticationEnabled = false} }; var basicAuthMock = new Mock(); - basicAuthMock.Setup(x => x.GetCredentials(It.IsAny(), It.IsAny())) - .Returns(basicCredential) + basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(basicCredential) .Verifiable(); var wiaAuthMock = new Mock(); @@ -96,7 +96,7 @@ public async Task GenericHostProvider_CreateCredentialAsync_WiaNotAllowed_Return Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); wiaAuthMock.Verify(x => x.GetIsSupportedAsync(It.IsAny()), Times.Never); - basicAuthMock.Verify(x => x.GetCredentials(It.IsAny(), It.IsAny()), Times.Once); + basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -117,8 +117,8 @@ public async Task GenericHostProvider_CreateCredentialAsync_LegacyAuthorityBasic Settings = {LegacyAuthorityOverride = "basic"} }; var basicAuthMock = new Mock(); - basicAuthMock.Setup(x => x.GetCredentials(It.IsAny(), It.IsAny())) - .Returns(basicCredential) + basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(basicCredential) .Verifiable(); var wiaAuthMock = new Mock(); @@ -130,7 +130,7 @@ public async Task GenericHostProvider_CreateCredentialAsync_LegacyAuthorityBasic Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); wiaAuthMock.Verify(x => x.GetIsSupportedAsync(It.IsAny()), Times.Never); - basicAuthMock.Verify(x => x.GetCredentials(It.IsAny(), It.IsAny()), Times.Once); + basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -148,8 +148,8 @@ public async Task GenericHostProvider_CreateCredentialAsync_NonHttpProtocol_Retu var context = new TestCommandContext(); var basicAuthMock = new Mock(); - basicAuthMock.Setup(x => x.GetCredentials(It.IsAny(), It.IsAny())) - .Returns(basicCredential) + basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(basicCredential) .Verifiable(); var wiaAuthMock = new Mock(); @@ -161,7 +161,7 @@ public async Task GenericHostProvider_CreateCredentialAsync_NonHttpProtocol_Retu Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); wiaAuthMock.Verify(x => x.GetIsSupportedAsync(It.IsAny()), Times.Never); - basicAuthMock.Verify(x => x.GetCredentials(It.IsAny(), It.IsAny()), Times.Once); + basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny()), Times.Once); } [PlatformFact(Platforms.Posix)] @@ -194,7 +194,7 @@ private static async Task TestCreateCredentialAsync_ReturnsEmptyCredential(bool var context = new TestCommandContext(); var basicAuthMock = new Mock(); - basicAuthMock.Setup(x => x.GetCredentials(It.IsAny(), It.IsAny())) + basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny())) .Verifiable(); var wiaAuthMock = new Mock(); wiaAuthMock.Setup(x => x.GetIsSupportedAsync(It.IsAny())) @@ -207,7 +207,7 @@ private static async Task TestCreateCredentialAsync_ReturnsEmptyCredential(bool Assert.NotNull(credential); Assert.Equal(string.Empty, credential.Account); Assert.Equal(string.Empty, credential.Password); - basicAuthMock.Verify(x => x.GetCredentials(It.IsAny(), It.IsAny()), Times.Never); + basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny()), Times.Never); } private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(bool wiaSupported) @@ -224,8 +224,8 @@ private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(bool var context = new TestCommandContext(); var basicAuthMock = new Mock(); - basicAuthMock.Setup(x => x.GetCredentials(It.IsAny(), It.IsAny())) - .Returns(basicCredential) + basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(basicCredential) .Verifiable(); var wiaAuthMock = new Mock(); wiaAuthMock.Setup(x => x.GetIsSupportedAsync(It.IsAny())) @@ -238,7 +238,7 @@ private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(bool Assert.NotNull(credential); Assert.Equal(testUserName, credential.Account); Assert.Equal(testPassword, credential.Password); - basicAuthMock.Verify(x => x.GetCredentials(It.IsAny(), It.IsAny()), Times.Once); + basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny(), It.IsAny()), Times.Once); } #endregion diff --git a/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml b/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml index 05044ae2d..33e285d77 100644 --- a/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml +++ b/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml @@ -10,6 +10,7 @@ + diff --git a/src/shared/Core.UI.Avalonia/AvaloniaUi.cs b/src/shared/Core.UI.Avalonia/AvaloniaUi.cs index a7e698f58..0b0bea8fb 100644 --- a/src/shared/Core.UI.Avalonia/AvaloniaUi.cs +++ b/src/shared/Core.UI.Avalonia/AvaloniaUi.cs @@ -88,6 +88,15 @@ private static Task ShowWindowInternal(Func windowFunc, object dataConte window.Activate(); window.Focus(); + // Workaround an issue where "Activate()" and "Focus()" don't actually + // cause the window to become the top-most window. Avalonia is correctly + // calling 'makeKeyAndOrderFront' but this isn't working for some reason. + if (PlatformUtils.IsMacOS()) + { + window.Topmost = true; + window.Topmost = false; + } + return tcs.Task; } diff --git a/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml b/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml index 1164a06d4..e05c0251f 100644 --- a/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml +++ b/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml @@ -7,13 +7,6 @@ Title="About Git Credential Manager" CanResize="False" Width="300" SizeToContent="Height" Background="#F6F6F6"> - - - - - - - @@ -37,6 +30,6 @@ + Text="Copyright © GitHub"/> diff --git a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml b/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml index 3ca500e90..f995a8790 100644 --- a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml +++ b/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml @@ -10,7 +10,7 @@ ExtendClientAreaChromeHints="{Binding ShowCustomChrome, Converter={x:Static converters:WindowClientAreaConverters.BoolToChromeHints}}" Title="{Binding Title}" SizeToContent="Height" CanResize="False" - Width="420" MaxHeight="520" MinHeight="320" + Width="420" MaxHeight="520" MinHeight="280" WindowState="Normal" WindowStartupLocation="CenterScreen" ShowInTaskbar="True" ShowActivated="True" PointerPressed="Window_PointerPressed" diff --git a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj b/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj index a7ade3aaa..961f67649 100644 --- a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj +++ b/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj @@ -11,9 +11,9 @@ - - - + + + diff --git a/src/shared/Core.UI/Commands/CredentialsCommand.cs b/src/shared/Core.UI/Commands/CredentialsCommand.cs new file mode 100644 index 000000000..02da8472f --- /dev/null +++ b/src/shared/Core.UI/Commands/CredentialsCommand.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading; +using System.Threading.Tasks; +using GitCredentialManager.UI.ViewModels; + +namespace GitCredentialManager.UI.Commands +{ + public abstract class CredentialsCommand : HelperCommand + { + protected CredentialsCommand(ICommandContext context) + : base(context, "basic", "Show basic authentication prompt.") + { + AddOption( + new Option("--title", "Window title (optional).") + ); + + AddOption( + new Option("--resource", "Resource name or URL (optional).") + ); + + AddOption( + new Option("--username", "User name (optional).") + ); + + AddOption( + new Option("--no-logo", "Hide the Git Credential Manager logo and logotype.") + ); + + Handler = CommandHandler.Create(ExecuteAsync); + } + + private class CommandOptions + { + public string Title { get; set; } + public string Resource { get; set; } + public string UserName { get; set; } + public bool NoLogo { get; set; } + } + + private async Task ExecuteAsync(CommandOptions options) + { + var viewModel = new CredentialsViewModel(); + + viewModel.Title = !string.IsNullOrWhiteSpace(options.Title) + ? options.Title + : "Git Credential Manager"; + + viewModel.Description = !string.IsNullOrWhiteSpace(options.Resource) + ? $"Enter your credentials for '{options.Resource}'" + : "Enter your credentials"; + + if (!string.IsNullOrWhiteSpace(options.UserName)) + { + viewModel.UserName = options.UserName; + } + + viewModel.ShowProductHeader = !options.NoLogo; + + await ShowAsync(viewModel, CancellationToken.None); + + if (!viewModel.WindowResult) + { + throw new Exception("User cancelled dialog."); + } + + WriteResult( + new Dictionary + { + ["username"] = viewModel.UserName, + ["password"] = viewModel.Password + } + ); + return 0; + } + + protected abstract Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct); + } +} diff --git a/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs b/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs new file mode 100644 index 000000000..c93c8ff29 --- /dev/null +++ b/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs @@ -0,0 +1,68 @@ +using System.ComponentModel; + +namespace GitCredentialManager.UI.ViewModels +{ + public class CredentialsViewModel : WindowViewModel + { + private string _userName; + private string _password; + private string _description; + private bool _showProductHeader; + private RelayCommand _signInCommand; + + public CredentialsViewModel() + { + SignInCommand = new RelayCommand(Accept, CanSignIn); + PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(UserName): + case nameof(Password): + SignInCommand.RaiseCanExecuteChanged(); + break; + } + } + + private bool CanSignIn() + { + // Allow empty username or empty password, or both! + // This is what the older Windows API CredUIPromptForWindowsCredentials + // permitted so we should continue to support any possible scenarios. + return true; + } + + public string UserName + { + get => _userName; + set => SetAndRaisePropertyChanged(ref _userName, value); + } + + public string Password + { + get => _password; + set => SetAndRaisePropertyChanged(ref _password, value); + } + + public string Description + { + get => _description; + set => SetAndRaisePropertyChanged(ref _description, value); + } + + public bool ShowProductHeader + { + get => _showProductHeader; + set => _showProductHeader = value; + } + + public RelayCommand SignInCommand + { + get => _signInCommand; + set => SetAndRaisePropertyChanged(ref _signInCommand, value); + } + } +} diff --git a/src/shared/Core/ApplicationBase.cs b/src/shared/Core/ApplicationBase.cs index a084b0f6c..55e173381 100644 --- a/src/shared/Core/ApplicationBase.cs +++ b/src/shared/Core/ApplicationBase.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -84,31 +83,18 @@ public Task RunAsync(string[] args) public static string GetEntryApplicationPath() { -#if NETFRAMEWORK - // Single file publishing does not exist with .NET Framework so - // we can just use reflection to get the entry assembly path. - return Assembly.GetEntryAssembly().Location; -#else - // Assembly::Location always returns an empty string if the application - // was published as a single file -#pragma warning disable IL3000 - bool isSingleFile = string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location); -#pragma warning restore IL3000 - - // Use "argv[0]" to get the full path to the entry executable in - // .NET 5+ when published as a single file. - string[] args = Environment.GetCommandLineArgs(); - string candidatePath = args[0]; - - // If we have not been published as a single file then we must strip the - // ".dll" file extension to get the default AppHost/SuperHost name. - if (!isSingleFile && Path.HasExtension(candidatePath)) + string argv0 = PlatformUtils.GetNativeArgv0() + ?? Process.GetCurrentProcess().MainModule?.FileName + ?? Environment.GetCommandLineArgs()[0]; + + if (Path.IsPathRooted(argv0)) { - return Path.ChangeExtension(candidatePath, null); + return argv0; } - return candidatePath; -#endif + return Path.GetFullPath( + Path.Combine(Environment.CurrentDirectory, argv0) + ); } /// diff --git a/src/shared/Core/Authentication/AuthenticationBase.cs b/src/shared/Core/Authentication/AuthenticationBase.cs index fdbe75a11..21eed2ba3 100644 --- a/src/shared/Core/Authentication/AuthenticationBase.cs +++ b/src/shared/Core/Authentication/AuthenticationBase.cs @@ -110,58 +110,111 @@ protected void ThrowIfTerminalPromptsDisabled() } } - protected bool TryFindHelperExecutablePath(string envar, string configName, string defaultValue, out string path) + protected bool TryFindHelperCommand(string envar, string configName, string defaultValue, out string command, out string args) { - bool isOverride = false; + command = null; + args = null; + + // + // Search for UI helpers with the following precedence and logic.. + // + // 1. (unset): use the default helper name that's in the source code and go to #3 + // 2. : use the absolute path only and exactly as entered + // 3. : search for.. + // a. /(.exe) - run this directly + // b. /(.dll) - use `dotnet exec` to run + // c. Search PATH for (.exe) - run this directly + // NOTE: do NOT search PATH for (.dll) as we don't know if this is a dotnet executable.. + // + // We print warning messages for missing helpers specified by the user, not the in-box ones. + // if (Context.Settings.TryGetPathSetting( - envar, Constants.GitConfiguration.Credential.SectionName, configName, out string helperName)) + envar, Constants.GitConfiguration.Credential.SectionName, configName, out string helperName)) { + // If the user set the helper override to the empty string then they are signalling not to use a helper + if (string.IsNullOrEmpty(helperName)) + { + Context.Trace.WriteLine("UI helper override specified as the empty string."); + return false; + } + Context.Trace.WriteLine($"UI helper override specified: '{helperName}'."); - isOverride = true; } else { - // Use the default helper if none was specified. - // On Windows append ".exe" for the default helpers only. If a user has specified their own - // helper they should append the correct extension. - helperName = PlatformUtils.IsWindows() ? $"{defaultValue}.exe" : defaultValue; + Context.Trace.WriteLine($"Using default UI helper: '{defaultValue}'."); + helperName = defaultValue; } - // If the user set the helper override to the empty string then they are signalling not to use a helper - if (string.IsNullOrEmpty(helperName)) + // + // Check for an absolute path.. run this directly without intermediaries or modification + // + if (Path.IsPathRooted(helperName)) { - path = null; + if (Context.FileSystem.FileExists(helperName)) + { + Context.Trace.WriteLine($"UI helper found at '{helperName}'."); + command = helperName; + return true; + } + + Context.Trace.WriteLine($"UI helper was not found at '{helperName}'."); + Context.Streams.Error.WriteLine($"warning: could not find configured UI helper '{helperName}'"); return false; } - if (Path.IsPathRooted(helperName)) - { - path = helperName; - } - else + // + // Search the installation directory for an in-box helper + // + string appDir = Path.GetDirectoryName(Context.ApplicationPath); + string inBoxExePath = Path.Combine(appDir, PlatformUtils.IsWindows() ? $"{helperName}.exe" : helperName); + string inBoxDllPath = Path.Combine(appDir, $"{helperName}.dll"); + + // Look for in-box native executables + if (Context.FileSystem.FileExists(inBoxExePath)) { - string executableDirectory = Path.GetDirectoryName(Context.ApplicationPath); - path = Path.Combine(executableDirectory!, helperName); + Context.Trace.WriteLine($"Found in-box native UI helper: '{inBoxExePath}'"); + command = inBoxExePath; + return true; } - if (!Context.FileSystem.FileExists(path)) + // Look for in-box .NET framework-dependent executables + if (Context.FileSystem.FileExists(inBoxDllPath)) { - // Only warn for missing helpers specified by the user, not in-box ones - if (isOverride) + string dotnetName = PlatformUtils.IsWindows() ? "dotnet.exe" : "dotnet"; + if (!Context.Environment.TryLocateExecutable(dotnetName, out string dotnetPath)) { - Context.Trace.WriteLine($"UI helper '{helperName}' was not found at '{path}'."); - Context.Streams.Error.WriteLine($"warning: could not find configured UI helper '{helperName}'"); + Context.Trace.WriteLine($"Unable to run UI helper '{inBoxDllPath}' without the .NET CLI."); + Context.Streams.Error.WriteLine($"warning: could not find .NET CLI to run UI helper '{inBoxDllPath}'"); + return false; } - return false; + Context.Trace.WriteLine($"Found in-box framework-dependent UI helper: '{inBoxDllPath}'"); + command = dotnetPath; + args = $"exec {QuoteCmdArg(inBoxDllPath)} "; + return true; + } + + // + // Search the PATH for a native executable (do NOT search for out-of-box .NET framework-dependent DLLs) + // + if (Context.Environment.TryLocateExecutable(helperName, out command)) + { + Context.Trace.WriteLine($"Found UI helper on PATH: '{helperName}'"); + return true; } - return true; + // + // No helper found! + // + Context.Trace.WriteLine($"UI helper '{helperName}' was not found."); + Context.Streams.Error.WriteLine($"warning: could not find UI helper '{helperName}'"); + return false; } public static string QuoteCmdArg(string str) { - char[] needsQuoteChars = {'"', ' ', '\\', '\n', '\r', '\t'}; + char[] needsQuoteChars = { '"', ' ', '\\', '\n', '\r', '\t' }; bool needsQuotes = str.Any(x => needsQuoteChars.Contains(x)); if (!needsQuotes) diff --git a/src/shared/Core/Authentication/BasicAuthentication.cs b/src/shared/Core/Authentication/BasicAuthentication.cs index 0fac4d32b..9d485105e 100644 --- a/src/shared/Core/Authentication/BasicAuthentication.cs +++ b/src/shared/Core/Authentication/BasicAuthentication.cs @@ -1,17 +1,20 @@ using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; namespace GitCredentialManager.Authentication { public interface IBasicAuthentication { - ICredential GetCredentials(string resource, string userName); + Task GetCredentialsAsync(string resource, string userName); } public static class BasicAuthenticationExtensions { - public static ICredential GetCredentials(this IBasicAuthentication basicAuth, string resource) + public static Task GetCredentialsAsync(this IBasicAuthentication basicAuth, string resource) { - return basicAuth.GetCredentials(resource, null); + return basicAuth.GetCredentialsAsync(resource, null); } } @@ -25,17 +28,16 @@ public class BasicAuthentication : AuthenticationBase, IBasicAuthentication public BasicAuthentication(ICommandContext context) : base (context) { } - public ICredential GetCredentials(string resource, string userName) + public async Task GetCredentialsAsync(string resource, string userName) { EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource)); ThrowIfUserInteractionDisabled(); - // TODO: we only support system GUI prompts on Windows currently if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession && - PlatformUtils.IsWindows()) + TryFindHelperCommand(out string command, out string args)) { - return GetCredentialsByUi(resource, userName); + return await GetCredentialsByUiAsync(command, args, resource, userName); } ThrowIfTerminalPromptsDisabled(); @@ -64,14 +66,44 @@ private ICredential GetCredentialsByTty(string resource, string userName) return new GitCredential(userName, password); } - private ICredential GetCredentialsByUi(string resource, string userName) + private async Task GetCredentialsByUiAsync(string command, string args, string resource, string userName) { - if (!Context.SystemPrompts.ShowCredentialPrompt(resource, userName, out ICredential credential)) + var promptArgs = new StringBuilder(args); + promptArgs.Append("basic"); + + if (!string.IsNullOrWhiteSpace(resource)) + { + promptArgs.AppendFormat(" --resource {0}", QuoteCmdArg(resource)); + } + + if (!string.IsNullOrWhiteSpace(userName)) + { + promptArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName)); + } + + IDictionary resultDict = await InvokeHelperAsync(command, promptArgs.ToString(), null); + + if (!resultDict.TryGetValue("username", out userName)) + { + throw new Exception("Missing 'username' in response"); + } + + if (!resultDict.TryGetValue("password", out string password)) { - throw new Exception("User cancelled the authentication prompt."); + throw new Exception("Missing 'password' in response"); } - return credential; + return new GitCredential(userName, password); + } + + private bool TryFindHelperCommand(out string command, out string args) + { + return TryFindHelperCommand( + Constants.EnvironmentVariables.GcmUiHelper, + Constants.GitConfiguration.Credential.UiHelper, + Constants.DefaultUiHelper, + out command, + out args); } } } diff --git a/src/shared/Core/Authentication/OAuth/OAuth2Client.cs b/src/shared/Core/Authentication/OAuth/OAuth2Client.cs index f46b61de7..9558a1f66 100644 --- a/src/shared/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/shared/Core/Authentication/OAuth/OAuth2Client.cs @@ -65,16 +65,20 @@ public class OAuth2Client : IOAuth2Client private readonly Uri _redirectUri; private readonly string _clientId; private readonly string _clientSecret; + private readonly ITrace _trace; + private readonly bool _addAuthHeader; private IOAuth2CodeGenerator _codeGenerator; - public OAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri = null, string clientSecret = null) + public OAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri = null, string clientSecret = null, ITrace trace = null, bool addAuthHeader = true) { _httpClient = httpClient; _endpoints = endpoints; _clientId = clientId; _redirectUri = redirectUri; _clientSecret = clientSecret; + _trace = trace; + _addAuthHeader = addAuthHeader; } public IOAuth2CodeGenerator CodeGenerator @@ -83,6 +87,18 @@ public IOAuth2CodeGenerator CodeGenerator set => _codeGenerator = value; } + protected string ClientId => _clientId; + + protected string ClientSecret => _clientSecret; + + protected ITrace Trace => _trace; + + protected OAuth2ServerEndpoints Endpoints => _endpoints; + + protected HttpClient HttpClient => _httpClient; + + protected Uri RedirectUri => _redirectUri; + #region IOAuth2Client public async Task GetAuthorizationCodeAsync(IEnumerable scopes, IOAuth2WebBrowser browser, CancellationToken ct) @@ -188,7 +204,8 @@ public async Task GetTokenByAuthorizationCodeAsync(OAuth2Auth [OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.AuthorizationCodeGrantType, [OAuth2Constants.TokenEndpoint.AuthorizationCodeParameter] = authorizationCodeResult.Code, [OAuth2Constants.TokenEndpoint.PkceVerifierParameter] = authorizationCodeResult.CodeVerifier, - [OAuth2Constants.ClientIdParameter] = _clientId + [OAuth2Constants.ClientIdParameter] = _clientId, + [OAuth2Constants.ClientSecretParameter] = _clientSecret }; if (authorizationCodeResult.RedirectUri != null) @@ -202,7 +219,7 @@ public async Task GetTokenByAuthorizationCodeAsync(OAuth2Auth } using (HttpContent requestContent = new FormUrlEncodedContent(formData)) - using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, true)) + using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, _addAuthHeader)) using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct)) { string json = await response.Content.ReadAsStringAsync(); @@ -223,6 +240,7 @@ public async Task GetTokenByRefreshTokenAsync(string refreshT [OAuth2Constants.TokenEndpoint.GrantTypeParameter] = OAuth2Constants.TokenEndpoint.RefreshTokenGrantType, [OAuth2Constants.TokenEndpoint.RefreshTokenParameter] = refreshToken, [OAuth2Constants.ClientIdParameter] = _clientId, + [OAuth2Constants.ClientSecretParameter] = _clientSecret }; if (_redirectUri != null) @@ -231,7 +249,7 @@ public async Task GetTokenByRefreshTokenAsync(string refreshT } using (HttpContent requestContent = new FormUrlEncodedContent(formData)) - using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, true)) + using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, _endpoints.TokenEndpoint, requestContent, _addAuthHeader)) using (HttpResponseMessage response = await _httpClient.SendAsync(request, ct)) { string json = await response.Content.ReadAsStringAsync(); @@ -345,7 +363,7 @@ private HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri requestUr return request; } - private Exception CreateExceptionFromResponse(string json) + protected Exception CreateExceptionFromResponse(string json) { if (TryCreateExceptionFromResponse(json, out OAuth2Exception exception)) { diff --git a/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs b/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs index a890044cf..d630d0282 100644 --- a/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs +++ b/src/shared/Core/Authentication/OAuth/OAuth2Constants.cs @@ -4,6 +4,7 @@ namespace GitCredentialManager.Authentication.OAuth public static class OAuth2Constants { public const string ClientIdParameter = "client_id"; + public const string ClientSecretParameter = "client_secret"; public const string RedirectUriParameter = "redirect_uri"; public const string ScopeParameter = "scope"; diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index 397d57bbb..0e784992a 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -66,11 +66,6 @@ public interface ICommandContext : IDisposable /// The current process environment. /// IEnvironment Environment { get; } - - /// - /// Native UI prompts. - /// - ISystemPrompts SystemPrompts { get; } } /// @@ -90,7 +85,6 @@ public CommandContext(string appPath) { FileSystem = new WindowsFileSystem(); SessionManager = new WindowsSessionManager(); - SystemPrompts = new WindowsSystemPrompts(); Environment = new WindowsEnvironment(FileSystem); Terminal = new WindowsTerminal(Trace); string gitPath = GetGitPath(Environment, FileSystem, Trace); @@ -106,7 +100,6 @@ public CommandContext(string appPath) { FileSystem = new MacOSFileSystem(); SessionManager = new MacOSSessionManager(); - SystemPrompts = new MacOSSystemPrompts(); Environment = new MacOSEnvironment(FileSystem); Terminal = new MacOSTerminal(Trace); string gitPath = GetGitPath(Environment, FileSystem, Trace); @@ -123,7 +116,6 @@ public CommandContext(string appPath) FileSystem = new LinuxFileSystem(); // TODO: support more than just 'Posix' or X11 SessionManager = new PosixSessionManager(); - SystemPrompts = new LinuxSystemPrompts(); Environment = new PosixEnvironment(FileSystem); Terminal = new LinuxTerminal(Trace); string gitPath = GetGitPath(Environment, FileSystem, Trace); @@ -142,9 +134,6 @@ public CommandContext(string appPath) HttpClientFactory = new HttpClientFactory(FileSystem, Trace, Settings, Streams); CredentialStore = new CredentialStore(this); - - // Set the parent window handle/ID - SystemPrompts.ParentWindowId = Settings.ParentWindowId; } private static string GetGitPath(IEnvironment environment, IFileSystem fileSystem, ITrace trace) @@ -203,8 +192,6 @@ private static string GetGitPath(IEnvironment environment, IFileSystem fileSyste public IEnvironment Environment { get; } - public ISystemPrompts SystemPrompts { get; } - #endregion #region IDisposable diff --git a/src/shared/Core/Commands/GetCommand.cs b/src/shared/Core/Commands/GetCommand.cs index 58653eb25..8cc1bff7d 100644 --- a/src/shared/Core/Commands/GetCommand.cs +++ b/src/shared/Core/Commands/GetCommand.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -35,6 +36,9 @@ protected override async Task ExecuteInternalAsync(InputArguments input, IHostPr output["username"] = credential.Account; output["password"] = credential.Password; + Context.Trace.WriteLine("Writing credentials to output:"); + Context.Trace.WriteDictionarySecrets(output, new []{ "password" }, StringComparer.OrdinalIgnoreCase); + // Write the values to standard out Context.Streams.Out.WriteDictionary(output); } diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index e3a1d7d9d..54c8b1246 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -8,6 +8,7 @@ public static class Constants public const string PersonalAccessTokenUserName = "PersonalAccessToken"; public const string DefaultCredentialNamespace = "git"; public const int DefaultAutoDetectProviderTimeoutMs = 2000; // 2 seconds + public const string DefaultUiHelper = "git-credential-manager-ui"; public const string ProviderIdAuto = "auto"; public const string AuthorityIdAuto = "auto"; @@ -87,6 +88,7 @@ public static class EnvironmentVariables public const string GpgExecutablePath = "GCM_GPG_PATH"; public const string GcmAutoDetectTimeout = "GCM_AUTODETECT_TIMEOUT"; public const string GcmGuiPromptsEnabled = "GCM_GUI_PROMPT"; + public const string GcmUiHelper = "GCM_UI_HELPER"; } public static class Http @@ -122,6 +124,7 @@ public static class Credential public const string UserName = "username"; public const string AutoDetectTimeout = "autoDetectTimeout"; public const string GuiPromptsEnabled = "guiPrompt"; + public const string UiHelper = "uiHelper"; } public static class Http @@ -158,6 +161,7 @@ public static class HelpUrls public const string GcmCredentialStores = "https://aka.ms/gcm/credstores"; public const string GcmWamComSecurity = "https://aka.ms/gcm/wamadmin"; public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect"; + public const string GcmExecRename = "https://aka.ms/gcm/rename"; } private static Version _gcmVersion; diff --git a/src/shared/Core/Core.csproj b/src/shared/Core/Core.csproj index 74b75b713..060554328 100644 --- a/src/shared/Core/Core.csproj +++ b/src/shared/Core/Core.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/shared/Core/Diagnostics/GitDiagnostic.cs b/src/shared/Core/Diagnostics/GitDiagnostic.cs index fe6a326a0..8066f2aaf 100644 --- a/src/shared/Core/Diagnostics/GitDiagnostic.cs +++ b/src/shared/Core/Diagnostics/GitDiagnostic.cs @@ -32,8 +32,10 @@ protected override Task RunInternalAsync(StringBuilder log, IList log.Append("Listing all Git configuration..."); Process configProc = _git.CreateProcess("config --list --show-origin"); configProc.Start(); - configProc.WaitForExit(); + // To avoid deadlocks, always read the output stream first and then wait + // TODO: don't read in all the data at once; stream it string gitConfig = configProc.StandardOutput.ReadToEnd().TrimEnd(); + configProc.WaitForExit(); log.AppendLine(" OK"); log.AppendLine("Git configuration:"); log.AppendLine(gitConfig); diff --git a/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs b/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs index 14d4585cf..6faee71fd 100644 --- a/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs +++ b/src/shared/Core/Diagnostics/NetworkingDiagnostic.cs @@ -25,23 +25,11 @@ public NetworkingDiagnostic(IHttpClientFactory httpFactory) protected override async Task RunInternalAsync(StringBuilder log, IList additionalFiles) { - log.AppendLine("Checking basic networking and HTTP stack..."); + log.AppendLine("Checking networking and HTTP stack..."); log.Append("Creating HTTP client..."); - using var rawHttpClient = new HttpClient(); + using var httpClient = _httpFactory.CreateClient(); log.AppendLine(" OK"); - if (!await RunTestAsync(log, rawHttpClient)) return false; - - log.AppendLine("Testing with IHttpClientFactory created HTTP client..."); - log.Append("Creating HTTP client..."); - using var contextHttpClient = _httpFactory.CreateClient(); - log.AppendLine(" OK"); - - return await RunTestAsync(log, rawHttpClient); - } - - private static async Task RunTestAsync(StringBuilder log, HttpClient httpClient) - { bool hasNetwork = NetworkInterface.GetIsNetworkAvailable(); log.AppendLine($"IsNetworkAvailable: {hasNetwork}"); diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index c0e794d4d..3f98eab0f 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -87,7 +87,7 @@ public override async Task GenerateCredentialAsync(InputArguments i } Context.Trace.WriteLine("Prompting for basic credentials..."); - return _basicAuth.GetCredentials(uri.AbsoluteUri, input.UserName); + return await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName); } /// diff --git a/src/shared/Core/Interop/Linux/LinuxSystemPrompts.cs b/src/shared/Core/Interop/Linux/LinuxSystemPrompts.cs deleted file mode 100644 index 05342b0bf..000000000 --- a/src/shared/Core/Interop/Linux/LinuxSystemPrompts.cs +++ /dev/null @@ -1,13 +0,0 @@ - -namespace GitCredentialManager.Interop.Linux -{ - public class LinuxSystemPrompts : ISystemPrompts - { - public object ParentWindowId { get; set; } - - public bool ShowCredentialPrompt(string resource, string userName, out ICredential credential) - { - throw new System.NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/src/shared/Core/Interop/MacOS/MacOSSystemPrompts.cs b/src/shared/Core/Interop/MacOS/MacOSSystemPrompts.cs deleted file mode 100644 index 9cbac2583..000000000 --- a/src/shared/Core/Interop/MacOS/MacOSSystemPrompts.cs +++ /dev/null @@ -1,13 +0,0 @@ - -namespace GitCredentialManager.Interop.MacOS -{ - public class MacOSSystemPrompts : ISystemPrompts - { - public object ParentWindowId { get; set; } - - public bool ShowCredentialPrompt(string resource, string userName, out ICredential credential) - { - throw new System.NotImplementedException(); - } - } -} diff --git a/src/shared/Core/Interop/MacOS/Native/LibC.cs b/src/shared/Core/Interop/MacOS/Native/LibC.cs new file mode 100644 index 000000000..ee3f02008 --- /dev/null +++ b/src/shared/Core/Interop/MacOS/Native/LibC.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; + +namespace GitCredentialManager.Interop.MacOS.Native +{ + public static class LibC + { + private const string LibCLib = "libc"; + + [DllImport(LibCLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr _NSGetArgv(); + + [DllImport(LibCLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr _NSGetProgname(); + } +} diff --git a/src/shared/Core/Interop/Windows/Native/Kernel32.cs b/src/shared/Core/Interop/Windows/Native/Kernel32.cs index e911ac205..403ddf2d7 100644 --- a/src/shared/Core/Interop/Windows/Native/Kernel32.cs +++ b/src/shared/Core/Interop/Windows/Native/Kernel32.cs @@ -226,6 +226,31 @@ public static extern bool GetConsoleMode( public static extern bool SetConsoleMode( [In] SafeFileHandle consoleHandle, [In, MarshalAs(UnmanagedType.U4)] ConsoleMode consoleMode); + + /// + /// Retrieves the command-line string for the current process. + /// + /// The return value is the command-line string for the current process. + [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr GetCommandLine(); + + /// + /// Frees the specified local memory object and invalidates its handle. + /// + /// + /// A handle to the local memory object. + /// This handle is returned by either the LocalAlloc or LocalReAlloc function. + /// It is not safe to free memory allocated with GlobalAlloc. + /// + /// + /// If the function succeeds, the return value is NULL. + /// + /// If the function fails, the return value is equal to a handle to the local memory object. + /// + /// To get extended error information, call GetLastError. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr LocalFree(IntPtr ptr); } [Flags] diff --git a/src/shared/Core/Interop/Windows/Native/Shell32.cs b/src/shared/Core/Interop/Windows/Native/Shell32.cs new file mode 100644 index 000000000..edf31d53f --- /dev/null +++ b/src/shared/Core/Interop/Windows/Native/Shell32.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; + +namespace GitCredentialManager.Interop.Windows.Native +{ + public static class Shell32 + { + private const string LibraryName = "shell32.dll"; + + /// + /// Parses a Unicode command line string and returns an array of pointers + /// to the command line arguments, along with a count of such arguments, + /// in a way that is similar to the standard C run-time argv and argc values. + /// + /// + /// Pointer to a null-terminated Unicode string that contains the full command line. + /// If this parameter is an empty string the function returns the path to the current executable file. + /// + /// + /// Pointer to an int that receives the number of array elements returned, similar to argc. + /// + /// A pointer to an array of LPWSTR values, similar to argv. + [DllImport("Shell32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr CommandLineToArgvW(IntPtr lpCmdLine, out int pNumArgs); + } +} diff --git a/src/shared/Core/PlatformUtils.cs b/src/shared/Core/PlatformUtils.cs index 5d62deb56..a2653a58f 100644 --- a/src/shared/Core/PlatformUtils.cs +++ b/src/shared/Core/PlatformUtils.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; using GitCredentialManager.Interop.Posix.Native; @@ -175,6 +176,63 @@ public static bool IsElevatedUser() return false; } + #region Platform argv[0] Utils + + public static string GetNativeArgv0() + { + try + { + if (IsWindows()) + { + return GetWindowsArgv0(); + } + + if (IsMacOS()) + { + return GetMacOSArgv0(); + } + + if (IsLinux()) + { + return GetLinuxArgv0(); + } + } + catch + { + // If there are any issues getting the native argv[0] + // we should not throw, and certainly not crash! + // Just return null instead. + } + + return null; + } + + private static string GetLinuxArgv0() + { + string cmdline = File.ReadAllText("/proc/self/cmdline"); + return cmdline.Split('\0')[0]; + } + + private static string GetMacOSArgv0() + { + IntPtr ptr = Interop.MacOS.Native.LibC._NSGetArgv(); + IntPtr argvPtr = Marshal.ReadIntPtr(ptr); + IntPtr argv0Ptr = Marshal.ReadIntPtr(argvPtr); + return Marshal.PtrToStringAnsi(argv0Ptr); + } + + private static string GetWindowsArgv0() + { + IntPtr argvPtr = Interop.Windows.Native.Shell32.CommandLineToArgvW( + Interop.Windows.Native.Kernel32.GetCommandLine(), out _); + IntPtr argv0Ptr = Marshal.ReadIntPtr(argvPtr); + string argv0 = Marshal.PtrToStringAuto(argv0Ptr); + Interop.Windows.Native.Kernel32.LocalFree(argvPtr); + return argv0; + } + + #endregion + #region Platform information helper methods private static string GetOSType() diff --git a/src/shared/Core/UriExtensions.cs b/src/shared/Core/UriExtensions.cs index 074b1e84f..b98f0cdd4 100644 --- a/src/shared/Core/UriExtensions.cs +++ b/src/shared/Core/UriExtensions.cs @@ -48,7 +48,7 @@ public static bool TryGetUserInfo(this Uri uri, out string userName, out string return false; } - /* According to RFC 3986 section 3.2.1 (https://tools.ietf.org/html/rfc3986#section-3.2.1) + /* According to RFC 3986 section 3.2.1 (https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1) * the user information component of a URI should look like: * * url-encode(username):url-encode(password) diff --git a/src/shared/DotnetTool/DotnetTool.csproj b/src/shared/DotnetTool/DotnetTool.csproj new file mode 100644 index 000000000..f1ed5221f --- /dev/null +++ b/src/shared/DotnetTool/DotnetTool.csproj @@ -0,0 +1,15 @@ + + + net6.0 + true + dotnet-tool.nuspec + + + version=$(PackageVersion); + publishDir=$(PublishDir); + + true + $(ProjectOutPath)nupkg\$(Configuration)\ + false + + diff --git a/src/shared/DotnetTool/DotnetToolSettings.xml b/src/shared/DotnetTool/DotnetToolSettings.xml new file mode 100644 index 000000000..01233bfdd --- /dev/null +++ b/src/shared/DotnetTool/DotnetToolSettings.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/shared/DotnetTool/dotnet-tool.nuspec b/src/shared/DotnetTool/dotnet-tool.nuspec new file mode 100644 index 000000000..b76a5659c --- /dev/null +++ b/src/shared/DotnetTool/dotnet-tool.nuspec @@ -0,0 +1,18 @@ + + + + git-credential-manager + $version$ + Secure, cross-platform Git credential storage with authentication to Azure Repos, GitHub, and other popular Git hosting services. + git-credential-manager + images\icon.png + https://raw.githubusercontent.com/GitCredentialManager/git-credential-manager/main/assets/gcm-transparent.png + + + + + + + + + diff --git a/src/shared/DotnetTool/icon.png b/src/shared/DotnetTool/icon.png new file mode 100644 index 000000000..f2a1a2328 Binary files /dev/null and b/src/shared/DotnetTool/icon.png differ diff --git a/src/shared/DotnetTool/pack-tool.sh b/src/shared/DotnetTool/pack-tool.sh new file mode 100755 index 000000000..007a881f8 --- /dev/null +++ b/src/shared/DotnetTool/pack-tool.sh @@ -0,0 +1,140 @@ +#!/bin/bash +die () { + echo "$*" >&2 + exit 1 +} + +make_absolute () { + case "$1" in + /*) + echo "$1" + ;; + *) + echo "$PWD/$1" + ;; + esac +} + +##################################################################### +# Lay out +##################################################################### +# Parse script arguments +for i in "$@" +do +case "$i" in + --configuration=*) + CONFIGURATION="${i#*=}" + shift # past argument=value + ;; + --version=*) + VERSION="${i#*=}" + shift # past argument=value + ;; + *) + # unknown option + ;; +esac +done + +if [ -z "$VERSION" ]; then + die "--version was not set" +fi + +# Directories +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )" +SRC="$ROOT/src" +OUT="$ROOT/out" +GCM_SRC="$SRC/shared/Git-Credential-Manager" +GCM_UI_SRC="$SRC/shared/Git-Credential-Manager.UI.Avalonia" +BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI.Avalonia" +GITHUB_UI_SRC="$SRC/shared/GitHub.UI.Avalonia" +GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia" +DOTNET_TOOL="shared/DotnetTool" +PROJ_OUT="$OUT/$DOTNET_TOOL" + +CONFIGURATION="${CONFIGURATION:=Debug}" + +# Build parameters +FRAMEWORK=net6.0 + +# Outputs +OUTDIR="$PROJ_OUT/nupkg/$CONFIGURATION" +IMGOUT="$OUTDIR/images" +PAYLOAD="$OUTDIR/payload" +SYMBOLOUT="$OUTDIR/payload.sym" + +# Cleanup output directory +if [ -d "$OUTDIR" ]; then + echo "Cleaning existing output directory '$OUTDIR'..." + rm -rf "$OUTDIR" +fi + +# Ensure output directories exist +mkdir -p "$PAYLOAD" "$SYMBOLOUT" "$IMGOUT" + +if [ -z "$DOTNET_ROOT" ]; then + DOTNET_ROOT="$(dirname $(which dotnet))" +fi + +# Publish core application executables +echo "Publishing core application..." +$DOTNET_ROOT/dotnet publish "$GCM_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --output="$(make_absolute "$PAYLOAD")" \ + -p:UseAppHost=false || exit 1 + +echo "Publishing core UI helper..." +$DOTNET_ROOT/dotnet publish "$GCM_UI_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --output="$(make_absolute "$PAYLOAD")" \ + -p:UseAppHost=false || exit 1 + +echo "Publishing Bitbucket UI helper..." +$DOTNET_ROOT/dotnet publish "$BITBUCKET_UI_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --output="$(make_absolute "$PAYLOAD")" \ + -p:UseAppHost=false || exit 1 + +echo "Publishing GitHub UI helper..." +$DOTNET_ROOT/dotnet publish "$GITHUB_UI_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --output="$(make_absolute "$PAYLOAD")" \ + -p:UseAppHost=false || exit 1 + +echo "Publishing GitLab UI helper..." +$DOTNET_ROOT/dotnet publish "$GITLAB_UI_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --output="$(make_absolute "$PAYLOAD")" \ + -p:UseAppHost=false || exit 1 + +# Collect symbols +echo "Collecting managed symbols..." +mv "$PAYLOAD"/*.pdb "$SYMBOLOUT" || exit 1 + +# Copy DotnetToolSettings.xml file +echo "Copying out package configuration files..." +cp "$SRC/$DOTNET_TOOL/DotnetToolSettings.xml" "$PAYLOAD/" + +# Copy package icon image +echo "Copying images..." +cp "$SRC/$DOTNET_TOOL/icon.png" "$IMGOUT" || exit 1 + +echo "Build complete." + +##################################################################### +# Pack dotnet tool +##################################################################### +echo "Creating dotnet tool package..." + +dotnet pack "$SRC/$DOTNET_TOOL/DotnetTool.csproj" \ + /p:Configuration="$CONFIGURATION" \ + /p:PackageVersion="$VERSION" \ + /p:PublishDir="$OUTDIR/" + +echo "Dotnet tool pack complete." diff --git a/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/CredentialsCommandImpl.cs b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/CredentialsCommandImpl.cs new file mode 100644 index 000000000..876f95ab0 --- /dev/null +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/CredentialsCommandImpl.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using GitCredentialManager.UI.ViewModels; +using GitCredentialManager.UI.Views; + +namespace GitCredentialManager.UI.Commands +{ + public class CredentialsCommandImpl : CredentialsCommand + { + public CredentialsCommandImpl(ICommandContext context) : base(context) { } + + protected override Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct) + { + return AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), ct); + } + } +} diff --git a/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml b/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml new file mode 100644 index 000000000..e10e5c401 --- /dev/null +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + +