From 45ca69de55e6aa056e5968209a3516898734863e Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Wed, 14 Dec 2022 16:31:10 -0700 Subject: [PATCH 01/40] release: correct nupkg path for publishing When we began signing the .NET tool in 80cc677, we did not update the path used for publishing to nuget.org. Fixing with this change. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70ff1d6a3..9a54e0e88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -795,5 +795,5 @@ jobs: - name: Publish .NET tool to nuget.org run: | - dotnet nuget push dotnet-tool-sign/signed/*.nupkg \ + dotnet nuget push dotnet-tool-sign/*.nupkg \ --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json From b7f2c8355460b8ed18cf53251fa433f618f846eb Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Fri, 16 Dec 2022 13:05:41 -0700 Subject: [PATCH 02/40] secret service: fix error creating credential Multiple Linux users have reported that they are unable to use Secret Service as their credential store, as GCM throws the following error: sec_free: Assertion `cell->requested > 0' failed. The root cause is that we're using the libsecret secret_value_get() function to obtain secret data, then attempting to free the string with secret_password_free(). It appears that secret_password_free() is only meant to be used to free nonpageable memory [1], however. Removing this call fixes the issue, as verified with a successful git-credential-manager diagnose (which was previously failing with the above error). [1] secret_password_free manpage https://www.manpagez.com/html/libsecret-1/libsecret-1-0.18.6/libsecret-Password-storage.php#secret-password-free --- src/shared/Core/Interop/Linux/SecretServiceCollection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/Core/Interop/Linux/SecretServiceCollection.cs b/src/shared/Core/Interop/Linux/SecretServiceCollection.cs index 6939ea85d..6e87342a7 100644 --- a/src/shared/Core/Interop/Linux/SecretServiceCollection.cs +++ b/src/shared/Core/Interop/Linux/SecretServiceCollection.cs @@ -291,7 +291,6 @@ private static unsafe ICredential CreateCredentialFromItem(SecretItem* item) if (accountKeyPtr != IntPtr.Zero) Marshal.FreeHGlobal(accountKeyPtr); if (serviceKeyPtr != IntPtr.Zero) Marshal.FreeHGlobal(serviceKeyPtr); if (value != null) secret_value_unref(value); - if (passwordPtr != IntPtr.Zero) secret_password_free(passwordPtr); if (error != null) g_error_free(error); } } From 0a93a98860c19c5fe777f05d085de971cff3cf93 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 22 Dec 2022 09:05:19 +0100 Subject: [PATCH 03/40] docs(install): fix link to the ".NET tool" section At least in GitHub-flavored Markdown, that section title gets transformed into the anchor `net-tool`, not `.NET-tool`. Signed-off-by: Johannes Schindelin --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 6233eb3c9..cc51f9805 100644 --- a/docs/install.md +++ b/docs/install.md @@ -64,7 +64,7 @@ sudo /usr/local/share/gcm-core/uninstall.sh ### .NET tool :star: -See the [.NET tool](#.NET-tool) section below for instructions on this +See the [.NET tool](#net-tool) section below for instructions on this installation method. --- From 631bbed169d29f5909efdda7aa1c14abf49c590e Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Thu, 22 Dec 2022 11:24:11 -0700 Subject: [PATCH 04/40] release: fix tarball signing While we added PGP signatures for tarballs in 7baac73, we did not notice that, while ESRP returns a file with the tar.gz extension, it is actually the signature file, not the tarball itself. Correct with this change and validate tarball moving forward so it doesn't happen again! --- .github/workflows/release.yml | 41 +++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a54e0e88..83516cd7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -384,7 +384,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v3 with: - name: tmp.linux-build + name: linux-build path: | linux-build @@ -399,7 +399,11 @@ jobs: - name: Download artifacts uses: actions/download-artifact@v3 with: - name: tmp.linux-build + name: linux-build + + - name: Remove symbols + run: | + rm tar/*symbols* - uses: azure/login@v1 with: @@ -423,6 +427,12 @@ jobs: run: | python .github/run_esrp_signing.py deb $env:LINUX_KEY_CODE $env:LINUX_OP_CODE python .github/run_esrp_signing.py tar $env:LINUX_KEY_CODE $env:LINUX_OP_CODE + + - name: Re-name tarball signature file + shell: bash + run: | + signaturepath=$(find signed/*.tar.gz) + mv "$signaturepath" "${signaturepath%.tar.gz}.asc" - name: Upload signed tarball and Debian package uses: actions/upload-artifact@v3 @@ -624,9 +634,15 @@ jobs: - os: ubuntu-latest artifact: linux-sign command: git-credential-manager + description: debian + - os: ubuntu-latest + artifact: linux-build + command: git-credential-manager + description: tarball - os: macos-latest artifact: osx-x64-sign command: git-credential-manager + description: osx-x64 - os: windows-latest artifact: win-sign # Even when a standalone GCM version is installed, GitHub actions @@ -634,9 +650,11 @@ jobs: # Windows due to its placement on the PATH. For this reason, we use # the full path to our installation to validate the Windows version. command: "$PROGRAMFILES (x86)/Git Credential Manager/git-credential-manager.exe" + description: windows - os: ubuntu-latest artifact: dotnet-tool-sign command: git-credential-manager + description: dotnet-tool runs-on: ${{ matrix.component.os }} needs: [ osx-sign, win-sign, linux-sign, dotnet-tool-sign ] steps: @@ -654,7 +672,7 @@ jobs: name: ${{ matrix.component.artifact }} - name: Install Windows - if: contains(matrix.component.os, 'windows') + if: contains(matrix.component.description, 'windows') shell: pwsh run: | $exePaths = Get-ChildItem -Path ./signed/*.exe | %{$_.FullName} @@ -663,22 +681,30 @@ jobs: Start-Process -Wait -FilePath "$exePath" -ArgumentList "/SILENT /VERYSILENT /NORESTART" } - - name: Install Linux - if: contains(matrix.component.os, 'ubuntu') && contains(matrix.component.artifact, 'linux') + - name: Install Linux (Debian package) + if: contains(matrix.component.description, 'debian') run: | debpath=$(find ./*.deb) sudo apt install $debpath "${{ matrix.component.command }}" configure + + - name: Install Linux (tarball) + if: contains(matrix.component.description, 'tarball') + run: | + # Ensure we find only the source tarball, not the symbols + tarpath=$(find ./tar -name '*[[:digit:]].tar.gz') + tar -xvf $tarpath -C /usr/local/bin + "${{ matrix.component.command }}" configure - name: Install macOS - if: contains(matrix.component.os, 'macos') + if: contains(matrix.component.description, 'osx-x64') run: | # Only validate x64, given arm64 agents are not available pkgpath=$(find ./*.pkg) sudo installer -pkg $pkgpath -target / - name: Install .NET tool - if: contains(matrix.component.os, 'ubuntu') && contains(matrix.component.artifact, 'dotnet-tool') + if: contains(matrix.component.description, 'dotnet-tool') run: | nupkgpath=$(find ./*.nupkg) dotnet tool install -g --add-source $(dirname "$nupkgpath") git-credential-manager @@ -787,6 +813,7 @@ jobs: uploadDirectoryToRelease('osx-payload-and-symbols'), // Upload Linux artifacts + uploadDirectoryToRelease('linux-build/tar'), uploadDirectoryToRelease('linux-sign'), // Upload .NET tool package From 34c0426c2aed43d69927d96ba96bb8aee1478003 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Wed, 28 Dec 2022 09:05:57 -0700 Subject: [PATCH 05/40] release: force xz compression for Debian package The GCM Debian package currently fails to install on Debian with the following error: error: archive 'gcm-linux_amd64.2.0.886.deb' uses unknown compression for member 'control.tar.zst', giving up It appears that the version of dpkg that ships with Debian does not support zstd compression [1]. Enforcing xz compression resolves the issue. This also provides an opportunity to clean up some unused variables in pack.sh and ensure we check the architecture is found before attempting to use the ARCH variable. [1]: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=892664 --- src/linux/Packaging.Linux/pack.sh | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh index c853af72d..788517c38 100755 --- a/src/linux/Packaging.Linux/pack.sh +++ b/src/linux/Packaging.Linux/pack.sh @@ -12,10 +12,6 @@ OUT="$ROOT/out" PROJ_OUT="$OUT/linux/Packaging.Linux" INSTALLER_SRC="$SRC/osx/Installer.Mac" -# Product information -IDENTIFIER="com.microsoft.gitcredentialmanager" -INSTALL_LOCATION="/usr/local/share/gcm-core" - # Parse script arguments for i in "$@" do @@ -51,6 +47,10 @@ fi ARCH="`dpkg-architecture -q DEB_HOST_ARCH`" +if test -z "$ARCH"; then + die "Could not determine host architecture!" +fi + TAROUT="$PROJ_OUT/$CONFIGURATION/tar/" TARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION.tar.gz" SYMTARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION-symbols.tar.gz" @@ -60,10 +60,6 @@ DEBROOT="$DEBOUT/root" DEBPKG="$DEBOUT/gcm-linux_$ARCH.$VERSION.deb" mkdir -p "$DEBROOT" -if test -z "$ARCH"; then - die "Could not determine host architecture!" -fi - # Set full read, write, execute permissions for owner and just read and execute permissions for group and other echo "Setting file permissions..." /bin/chmod -R 755 "$PAYLOAD" || exit 1 @@ -114,8 +110,6 @@ Description: Cross Platform Git Credential Manager command line utility. For more information see https://aka.ms/gcm EOF -mkdir -p "$INSTALL_TO" "$LINK_TO" - # Copy all binaries and shared libraries to target installation location cp -R "$PAYLOAD"/* "$INSTALL_TO" || exit 1 @@ -131,6 +125,6 @@ if [ ! -f "$LINK_TO/git-credential-manager-core" ]; then "$LINK_TO/git-credential-manager-core" || exit 1 fi -dpkg-deb --build "$DEBROOT" "$DEBPKG" || exit 1 +dpkg-deb -Zxz --build "$DEBROOT" "$DEBPKG" || exit 1 echo $MESSAGE From a28811f13a91b5accac8ad20d96e57c6e04f0e71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 20:08:01 +0000 Subject: [PATCH 06/40] build(deps): bump DavidAnson/markdownlint-cli2-action Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/DavidAnson/markdownlint-cli2-action) from 8.0.0 to 9.0.0. - [Release notes](https://github.com/DavidAnson/markdownlint-cli2-action/releases) - [Commits](https://github.com/DavidAnson/markdownlint-cli2-action/compare/d57f8bd57670b9c1deedf71219dd494614ff3335...5b7c9f74fec47e6b15667b2cc23c63dff11e449e) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index fe7d53095..864ae2f73 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - - uses: DavidAnson/markdownlint-cli2-action@d57f8bd57670b9c1deedf71219dd494614ff3335 + - uses: DavidAnson/markdownlint-cli2-action@5b7c9f74fec47e6b15667b2cc23c63dff11e449e with: globs: | "**/*.md" From a5df43673cfb0f6df0a3a3a3842b4c672501e55e Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 10 Jan 2023 00:04:41 -0800 Subject: [PATCH 07/40] Swap order to match GitLab UI --- docs/gitlab.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index 58975b6cd..480976f0c 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -9,8 +9,8 @@ configuration: 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. +_Unselect_ the 'Confidential' option. Set the 'read_repository' and +'write_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 From 97f4ebd5eb9187e88be121c891b5b9cd42e3179b Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 10 Jan 2023 00:06:27 -0800 Subject: [PATCH 08/40] Update gitlab.md --- docs/gitlab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index 480976f0c..f3ff77954 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -2,7 +2,7 @@ Git Credential Manager supports [gitlab.com][gitlab] out the box. -## Using on a another instance +## Using on another instance To use on another instance, eg. `https://gitlab.example.com` requires setup and configuration: From bb09f617b0b0bb0b541a8f79fd749eb75834f47e Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 10 Jan 2023 00:11:14 -0800 Subject: [PATCH 09/40] Update gitlab.md --- docs/gitlab.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index f3ff77954..7dbd0418e 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -17,7 +17,7 @@ _Unselect_ the 'Confidential' option. Set the 'read_repository' and `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` +`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. @@ -74,7 +74,7 @@ If you have a preferred authentication mode, you can specify [credential.gitLabAuthModes][config-gitlab-auth-modes]: ```console -git config --global credential.gitlabauthmodes browser +git config --global credential.GitLabAuthModes browser ``` ## Caveats From b71abb1cddb10260cf5b90174cdfc513af62ce3a Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 10 Jan 2023 00:25:40 -0800 Subject: [PATCH 10/40] Update gitlab.md --- docs/gitlab.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index 7dbd0418e..0ff00ce78 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -9,8 +9,7 @@ configuration: 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 'read_repository' and -'write_repository' scopes. +_Unselect_ the 'Confidential' option. Set the 'write_repository' scope. 1. Copy the application ID and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientId ` 1. Copy the application secret and configure From f7d50a2eeaebd1ed523191fa9739371a2631deda Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 10 Jan 2023 00:28:03 -0800 Subject: [PATCH 11/40] Update gitlab.md --- docs/gitlab.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index 0ff00ce78..7dbd0418e 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -9,7 +9,8 @@ configuration: 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' scope. +_Unselect_ the 'Confidential' option. Set the 'read_repository' and +'write_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 From b091365e102ce3e9b6334523b97d61afb3758b5a Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 10 Jan 2023 00:40:14 -0800 Subject: [PATCH 12/40] Update gitlab.md --- docs/gitlab.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index 7dbd0418e..e13a4a98e 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -12,12 +12,12 @@ or instance level. Specify a name and use a redirect URI of `http://127.0.0.1/`. _Unselect_ the 'Confidential' option. Set the 'read_repository' and 'write_repository' scopes. 1. Copy the application ID and configure -`git config --global credential.https://gitlab.example.com.GitLabDevClientId ` +`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 +`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` +`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. @@ -27,8 +27,8 @@ This may be necessary to recognise the domain as a GitLab instance. ### Clearing config ```console - git config --global --unset-all credential.https://gitlab.example.com.GitLabDevClientId - git config --global --unset-all credential.https://gitlab.example.com.GitLabDevClientSecret + git config --global --unset-all credential.https://gitlab.example.com.gitLabDevClientId + git config --global --unset-all credential.https://gitlab.example.com.gitLabDevClientSecret git config --global --unset-all credential.https://gitlab.example.com.provider ``` @@ -39,23 +39,23 @@ 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 +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 +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 +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 +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 +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 +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). @@ -74,7 +74,7 @@ If you have a preferred authentication mode, you can specify [credential.gitLabAuthModes][config-gitlab-auth-modes]: ```console -git config --global credential.GitLabAuthModes browser +git config --global credential.gitLabAuthModes browser ``` ## Caveats From c7ed1395cd0d1feab5976a7f51b305c2d5dd185f Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 10 Jan 2023 00:42:27 -0800 Subject: [PATCH 13/40] Update gitlab.md --- docs/gitlab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index e13a4a98e..1ec7cb01c 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -16,7 +16,7 @@ _Unselect_ the 'Confidential' option. Set the 'read_repository' and 1. Copy the application secret and configure `git config --global credential.https://gitlab.example.com.gitLabDevClientSecret ` -1. Configure authentication modes to include 'browser' +1. Optional if you want to force OAuth: `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`. From 8141654839fbc51b2162834d86f007986ad60960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Derriey?= Date: Thu, 19 Jan 2023 08:34:12 +0100 Subject: [PATCH 14/40] Fix typo --- docs/rename.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rename.md b/docs/rename.md index b35d164e7..95e5dfde3 100644 --- a/docs/rename.md +++ b/docs/rename.md @@ -3,7 +3,7 @@ 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]. +project in its own [organization][gcm-org]. ![Git Credential Manager Core renamed](img/gcmcore-rename.png) From cf55a1cea7fc6f8ecb88f7f48cc9c1138b2e635d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Fri, 20 Jan 2023 15:16:35 -0800 Subject: [PATCH 15/40] Update gitlab.md --- docs/gitlab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index 1ec7cb01c..04b122e1c 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -16,7 +16,7 @@ _Unselect_ the 'Confidential' option. Set the 'read_repository' and 1. Copy the application secret and configure `git config --global credential.https://gitlab.example.com.gitLabDevClientSecret ` -1. Optional if you want to force OAuth: +1. Optional if you want to force browser auth: `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`. From d0e5964859f33e33c9bb2c1606d0b90de718b346 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Mon, 23 Jan 2023 09:53:54 -0700 Subject: [PATCH 16/40] docs: update git credential cache platforms GCM's documentation states that git's credential cache is supported on Windows. Unfortunately, due to lack of Unix socket support on Windows versions prior to Windows 10, this feature is not currently supported on Windows, so this change removes it from the list of platforms on which this credstore can be used. See [1] for more details. [1]: https://github.com/git-for-windows/git/issues/3892 --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 03272592a..d77819731 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -442,7 +442,7 @@ _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|- `keychain`|macOS Keychain.|macOS `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 +`cache`|Git's built-in [credential cache][credential-cache].|macOS, Linux `plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`][credential-plaintextstorepath].|Windows, macOS, Linux #### Example From e9ee7641f37df0f03655f37d0a39e8b1df6dc723 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 27 Jan 2023 11:44:14 -0800 Subject: [PATCH 17/40] basic-ui: fix bug in VM property Fix a bug in a view model property for the basic credentials prompt; we should be updating the backing field and also raising the PropertyChanged event. --- src/shared/Core.UI/ViewModels/CredentialsViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs b/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs index c93c8ff29..7f40b8bea 100644 --- a/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs +++ b/src/shared/Core.UI/ViewModels/CredentialsViewModel.cs @@ -56,7 +56,7 @@ public string Description public bool ShowProductHeader { get => _showProductHeader; - set => _showProductHeader = value; + set => SetAndRaisePropertyChanged(ref _showProductHeader, value); } public RelayCommand SignInCommand From d1d57244087ece2c6b0cd245aca751b856978a45 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Fri, 27 Jan 2023 18:12:15 +0000 Subject: [PATCH 18/40] linux: ensure symbols tarball contains symbols A user pointed out in a recent GCM issue that our symbols tarball does not actually contain symbols (see [1] for additional details). Fix the `pack.sh` script to package the correct files into the symbols tarball. [1]: https://github.com/GitCredentialManager/git-credential-manager/issues/1028#issuecomment-1382862721 --- src/linux/Packaging.Linux/build.sh | 3 ++- src/linux/Packaging.Linux/pack.sh | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index c407fd892..308530afe 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -44,6 +44,7 @@ fi OUTDIR="$INSTALLER_OUT/$CONFIGURATION" PAYLOAD="$OUTDIR/payload" +SYMBOLS="$OUTDIR/payload.sym" # Lay out payload "$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" || exit 1 @@ -78,7 +79,7 @@ if [ $INSTALL_FROM_SOURCE = true ]; then echo "Install complete." else # Pack - "$INSTALLER_SRC/pack.sh" --configuration="$CONFIGURATION" --payload="$PAYLOAD" --version="$VERSION" || exit 1 + "$INSTALLER_SRC/pack.sh" --configuration="$CONFIGURATION" --payload="$PAYLOAD" --symbols="$SYMBOLS" --version="$VERSION" || exit 1 fi echo "Build of Packaging.Linux complete." diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh index 788517c38..3c1072d55 100755 --- a/src/linux/Packaging.Linux/pack.sh +++ b/src/linux/Packaging.Linux/pack.sh @@ -24,6 +24,10 @@ case "$i" in PAYLOAD="${i#*=}" shift # past argument=value ;; + --symbols=*) + SYMBOLS="${i#*=}" + shift # past argument=value + ;; --configuration=*) CONFIGURATION="${i#*=}" shift # past argument=value @@ -44,6 +48,9 @@ if [ -z "$PAYLOAD" ]; then elif [ ! -d "$PAYLOAD" ]; then die "Could not find '$PAYLOAD'. Did you run layout.sh first?" fi +if [ -z "$SYMBOLS" ]; then + die "--symbols was not set" +fi ARCH="`dpkg-architecture -q DEB_HOST_ARCH`" @@ -83,7 +90,7 @@ popd # Build symbols tarball echo "Building symbols tarball..." -pushd "$SYMBOLOUT" +pushd "$SYMBOLS" tar -czvf "$SYMTARBALL" * || exit 1 popd From 717b8225f431b093399b385104eade0d8b3cf957 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 27 Jan 2023 11:49:01 -0800 Subject: [PATCH 19/40] generic: add ability to read generic OAuth config Teach the Generic host provider to read configuration for OAuth-based authentication. These are largely parameters required for the OAuth2Client to be constructed including Client ID/Secret, Redirect URI and Scopes. --- .../Core.Tests/GenericOAuthConfigTests.cs | 61 ++++++++ src/shared/Core/Constants.cs | 21 +++ src/shared/Core/GenericHostProvider.cs | 26 +++- src/shared/Core/GenericOAuthConfig.cs | 138 ++++++++++++++++++ .../Objects/TestSettings.cs | 34 ++++- 5 files changed, 268 insertions(+), 12 deletions(-) create mode 100644 src/shared/Core.Tests/GenericOAuthConfigTests.cs create mode 100644 src/shared/Core/GenericOAuthConfig.cs diff --git a/src/shared/Core.Tests/GenericOAuthConfigTests.cs b/src/shared/Core.Tests/GenericOAuthConfigTests.cs new file mode 100644 index 000000000..08dfacab4 --- /dev/null +++ b/src/shared/Core.Tests/GenericOAuthConfigTests.cs @@ -0,0 +1,61 @@ +using System; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace GitCredentialManager.Tests +{ + public class GenericOAuthConfigTests + { + [Fact] + public void GenericOAuthConfig_TryGet_Valid_ReturnsTrue() + { + var remoteUri = new Uri("https://example.com"); + const string expectedClientId = "115845b0-77f8-4c06-a3dc-7d277381fad1"; + const string expectedClientSecret = "4D35385D9F24"; + const string expectedUserName = "TEST_USER"; + const string authzEndpoint = "/oauth/authorize"; + const string tokenEndpoint = "/oauth/token"; + const string deviceEndpoint = "/oauth/device"; + string[] expectedScopes = { "scope1", "scope2" }; + var expectedRedirectUri = new Uri("http://localhost:12345"); + var expectedAuthzEndpoint = new Uri(remoteUri, authzEndpoint); + var expectedTokenEndpoint = new Uri(remoteUri, tokenEndpoint); + var expectedDeviceEndpoint = new Uri(remoteUri, deviceEndpoint); + + string GetKey(string name) => $"{Constants.GitConfiguration.Credential.SectionName}.https://example.com.{name}"; + + var trace = new NullTrace(); + var settings = new TestSettings + { + GitConfiguration = new TestGitConfiguration + { + Global = + { + [GetKey(Constants.GitConfiguration.Credential.OAuthClientId)] = new[] { expectedClientId }, + [GetKey(Constants.GitConfiguration.Credential.OAuthClientSecret)] = new[] { expectedClientSecret }, + [GetKey(Constants.GitConfiguration.Credential.OAuthRedirectUri)] = new[] { expectedRedirectUri.ToString() }, + [GetKey(Constants.GitConfiguration.Credential.OAuthScopes)] = new[] { string.Join(' ', expectedScopes) }, + [GetKey(Constants.GitConfiguration.Credential.OAuthAuthzEndpoint)] = new[] { authzEndpoint }, + [GetKey(Constants.GitConfiguration.Credential.OAuthTokenEndpoint)] = new[] { tokenEndpoint }, + [GetKey(Constants.GitConfiguration.Credential.OAuthDeviceEndpoint)] = new[] { deviceEndpoint }, + [GetKey(Constants.GitConfiguration.Credential.OAuthDefaultUserName)] = new[] { expectedUserName }, + } + }, + RemoteUri = remoteUri + }; + + bool result = GenericOAuthConfig.TryGet(trace, settings, remoteUri, out GenericOAuthConfig config); + + Assert.True(result); + Assert.Equal(expectedClientId, config.ClientId); + Assert.Equal(expectedClientSecret, config.ClientSecret); + Assert.Equal(expectedRedirectUri, config.RedirectUri); + Assert.Equal(expectedScopes, config.Scopes); + Assert.Equal(expectedAuthzEndpoint, config.Endpoints.AuthorizationEndpoint); + Assert.Equal(expectedTokenEndpoint, config.Endpoints.TokenEndpoint); + Assert.Equal(expectedDeviceEndpoint, config.Endpoints.DeviceAuthorizationEndpoint); + Assert.Equal(expectedUserName, config.DefaultUserName); + Assert.True(config.UseAuthHeader); + } + } +} diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 54c8b1246..b8c9fa750 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -89,6 +89,16 @@ public static class EnvironmentVariables public const string GcmAutoDetectTimeout = "GCM_AUTODETECT_TIMEOUT"; public const string GcmGuiPromptsEnabled = "GCM_GUI_PROMPT"; public const string GcmUiHelper = "GCM_UI_HELPER"; + public const string OAuthAuthenticationModes = "GCM_OAUTH_AUTHMODES"; + public const string OAuthClientId = "GCM_OAUTH_CLIENTID"; + public const string OAuthClientSecret = "GCM_OAUTH_CLIENTSECRET"; + public const string OAuthRedirectUri = "GCM_OAUTH_REDIRECTURI"; + public const string OAuthScopes = "GCM_OAUTH_SCOPES"; + public const string OAuthAuthzEndpoint = "GCM_OAUTH_AUTHORIZE_ENDPOINT"; + public const string OAuthTokenEndpoint = "GCM_OAUTH_TOKEN_ENDPOINT"; + public const string OAuthDeviceEndpoint = "GCM_OAUTH_DEVICE_ENDPOINT"; + public const string OAuthClientAuthHeader = "GCM_OAUTH_USE_CLIENT_AUTH_HEADER"; + public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME"; } public static class Http @@ -125,6 +135,17 @@ public static class Credential public const string AutoDetectTimeout = "autoDetectTimeout"; public const string GuiPromptsEnabled = "guiPrompt"; public const string UiHelper = "uiHelper"; + + public const string OAuthAuthenticationModes = "oauthAuthModes"; + public const string OAuthClientId = "oauthClientId"; + public const string OAuthClientSecret = "oauthClientSecret"; + public const string OAuthRedirectUri = "oauthRedirectUri"; + public const string OAuthScopes = "oauthScopes"; + public const string OAuthAuthzEndpoint = "oauthAuthorizeEndpoint"; + public const string OAuthTokenEndpoint = "oauthTokenEndpoint"; + public const string OAuthDeviceEndpoint = "oauthDeviceEndpoint"; + public const string OAuthClientAuthHeader = "oauthUseClientAuthHeader"; + public const string OAuthDefaultUserName = "oauthDefaultUserName"; } public static class Http diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index 3f98eab0f..18f288def 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -26,8 +26,6 @@ public GenericHostProvider(ICommandContext context, _winAuth = winAuth; } - #region HostProvider - public override string Id => "generic"; public override string Name => "Generic"; @@ -50,12 +48,29 @@ public override async Task GenerateCredentialAsync(InputArguments i Uri uri = input.GetRemoteUri(); - // Determine the if the host supports Windows Integration Authentication (WIA) + // Determine the if the host supports Windows Integration Authentication (WIA) or OAuth if (!StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "http") && !StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "https")) { - // Cannot check WIA support for non-HTTP based protocols + // Cannot check WIA or OAuth support for non-HTTP based protocols + } + // Check for an OAuth configuration for this remote + else if (GenericOAuthConfig.TryGet(Context.Trace, Context.Settings, uri, out GenericOAuthConfig oauthConfig)) + { + Context.Trace.WriteLine($"Found generic OAuth configuration for '{uri}':"); + Context.Trace.WriteLine($"\tAuthzEndpoint = {oauthConfig.Endpoints.AuthorizationEndpoint}"); + Context.Trace.WriteLine($"\tTokenEndpoint = {oauthConfig.Endpoints.TokenEndpoint}"); + Context.Trace.WriteLine($"\tDeviceEndpoint = {oauthConfig.Endpoints.DeviceAuthorizationEndpoint}"); + Context.Trace.WriteLine($"\tClientId = {oauthConfig.ClientId}"); + Context.Trace.WriteLine($"\tClientSecret = {oauthConfig.ClientSecret}"); + Context.Trace.WriteLine($"\tRedirectUri = {oauthConfig.RedirectUri}"); + Context.Trace.WriteLine($"\tScopes = [{string.Join(", ", oauthConfig.Scopes)}]"); + Context.Trace.WriteLine($"\tUseAuthHeader = {oauthConfig.UseAuthHeader}"); + Context.Trace.WriteLine($"\tDefaultUserName = {oauthConfig.DefaultUserName}"); + + throw new NotImplementedException(); } + // Try detecting WIA for this remote, if permitted else if (IsWindowsAuthAllowed) { if (PlatformUtils.IsWindows()) @@ -86,6 +101,7 @@ public override async Task GenerateCredentialAsync(InputArguments i Context.Trace.WriteLine("Windows Integrated Authentication detection has been disabled."); } + // Use basic authentication Context.Trace.WriteLine("Prompting for basic credentials..."); return await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName); } @@ -120,7 +136,5 @@ protected override void ReleaseManagedResources() _winAuth.Dispose(); base.ReleaseManagedResources(); } - - #endregion } } diff --git a/src/shared/Core/GenericOAuthConfig.cs b/src/shared/Core/GenericOAuthConfig.cs new file mode 100644 index 000000000..0e2a74b75 --- /dev/null +++ b/src/shared/Core/GenericOAuthConfig.cs @@ -0,0 +1,138 @@ +using System; +using GitCredentialManager.Authentication.OAuth; + +namespace GitCredentialManager +{ + public class GenericOAuthConfig + { + public static bool TryGet(ITrace trace, ISettings settings, Uri remoteUri, out GenericOAuthConfig config) + { + config = new GenericOAuthConfig(); + + if (!settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthAuthzEndpoint, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthAuthzEndpoint, + out string authzEndpoint) || + !Uri.TryCreate(remoteUri, authzEndpoint, out Uri authzEndpointUri)) + { + trace.WriteLine($"Invalid OAuth configuration - missing/invalid authorize endpoint: {authzEndpoint}"); + config = null; + return false; + } + + if (!settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthTokenEndpoint, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthTokenEndpoint, + out string tokenEndpoint) || + !Uri.TryCreate(remoteUri, tokenEndpoint, out Uri tokenEndpointUri)) + { + trace.WriteLine($"Invalid OAuth configuration - missing/invalid token endpoint: {tokenEndpoint}"); + config = null; + return false; + } + + // Device code endpoint is optional + Uri deviceEndpointUri = null; + if (settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthDeviceEndpoint, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthDeviceEndpoint, + out string deviceEndpoint)) + { + if (!Uri.TryCreate(remoteUri, deviceEndpoint, out deviceEndpointUri)) + { + trace.WriteLine($"Invalid OAuth configuration - invalid device endpoint: {deviceEndpoint}"); + } + } + + config.Endpoints = new OAuth2ServerEndpoints(authzEndpointUri, tokenEndpointUri) + { + DeviceAuthorizationEndpoint = deviceEndpointUri + }; + + if (settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthClientId, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthClientId, + out string clientId)) + { + config.ClientId = clientId; + } + + if (settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthClientSecret, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthClientSecret, + out string clientSecret)) + { + config.ClientSecret = clientSecret; + } + + if (settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthRedirectUri, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthRedirectUri, + out string redirectUrl) && + Uri.TryCreate(redirectUrl, UriKind.Absolute, out Uri redirectUri)) + { + config.RedirectUri = redirectUri; + } + else + { + trace.WriteLine($"Invalid OAuth configuration - missing/invalid redirect URI: {redirectUrl}"); + config = null; + return false; + } + + if (settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthScopes, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthScopes, + out string scopesStr) && !string.IsNullOrWhiteSpace(scopesStr)) + { + config.Scopes = scopesStr.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + } + else + { + config.Scopes = Array.Empty(); + } + + if (settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthClientAuthHeader, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthClientAuthHeader, + out string useHeader)) + { + config.UseAuthHeader = useHeader.IsTruthy(); + } + else + { + // Default to true + config.UseAuthHeader = true; + } + + config.DefaultUserName = settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthDefaultUserName, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthDefaultUserName, + out string userName) + ? userName + : "OAUTH_USER"; + + return true; + } + + + public OAuth2ServerEndpoints Endpoints { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public Uri RedirectUri { get; set; } + public string[] Scopes { get; set; } + public bool UseAuthHeader { get; set; } + public string DefaultUserName { get; set; } + + public bool SupportsDeviceCode => Endpoints.DeviceAuthorizationEndpoint != null; + } +} diff --git a/src/shared/TestInfrastructure/Objects/TestSettings.cs b/src/shared/TestInfrastructure/Objects/TestSettings.cs index 73a6ec37c..a7ea19abb 100644 --- a/src/shared/TestInfrastructure/Objects/TestSettings.cs +++ b/src/shared/TestInfrastructure/Objects/TestSettings.cs @@ -58,6 +58,18 @@ public bool TryGetSetting(string envarName, string section, string property, out return true; } + if (RemoteUri != null) + { + foreach (string scope in RemoteUri.GetGitConfigurationScopes()) + { + string key = $"{section}.{scope}.{property}"; + if (GitConfiguration?.TryGet(key, false, out value) ?? false) + { + return true; + } + } + } + if (GitConfiguration?.TryGet($"{section}.{property}", false, out value) ?? false) { return true; @@ -79,16 +91,26 @@ public IEnumerable GetSettingValues(string envarName, string section, st yield return envarValue; } - foreach (string scope in RemoteUri.GetGitConfigurationScopes()) + IEnumerable configValues; + if (RemoteUri != null) { - string key = $"{section}.{scope}.{property}"; - - IEnumerable configValues = GitConfiguration.GetAll(key); - foreach (string value in configValues) + foreach (string scope in RemoteUri.GetGitConfigurationScopes()) { - yield return value; + string key = $"{section}.{scope}.{property}"; + + configValues = GitConfiguration.GetAll(key); + foreach (string value in configValues) + { + yield return value; + } } } + + configValues = GitConfiguration.GetAll($"{section}.{property}"); + foreach (string value in configValues) + { + yield return value; + } } public string RepositoryPath { get; set; } From 720a078b2203e40ef079a176ca7832cfbc4f1c67 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 27 Jan 2023 11:56:32 -0800 Subject: [PATCH 20/40] generic: add OAuth support for browser & devicecode Add OAuth support for the generic provider offering browser (authcode grant) and device code (device auth grant) support. Device code and mode selection is initially only offered for TTY users. --- .../Core.Tests/GenericHostProviderTests.cs | 15 ++- .../Authentication/OAuthAuthentication.cs | 123 ++++++++++++++++++ src/shared/Core/GenericHostProvider.cs | 77 ++++++++++- 3 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 src/shared/Core/Authentication/OAuthAuthentication.cs diff --git a/src/shared/Core.Tests/GenericHostProviderTests.cs b/src/shared/Core.Tests/GenericHostProviderTests.cs index 42dc62177..19d6aa1eb 100644 --- a/src/shared/Core.Tests/GenericHostProviderTests.cs +++ b/src/shared/Core.Tests/GenericHostProviderTests.cs @@ -87,8 +87,9 @@ public async Task GenericHostProvider_CreateCredentialAsync_WiaNotAllowed_Return .ReturnsAsync(basicCredential) .Verifiable(); var wiaAuthMock = new Mock(); + var oauthMock = new Mock(); - var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object); + var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object); ICredential credential = await provider.GenerateCredentialAsync(input); @@ -121,8 +122,9 @@ public async Task GenericHostProvider_CreateCredentialAsync_LegacyAuthorityBasic .ReturnsAsync(basicCredential) .Verifiable(); var wiaAuthMock = new Mock(); + var oauthMock = new Mock(); - var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object); + var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object); ICredential credential = await provider.GenerateCredentialAsync(input); @@ -152,8 +154,9 @@ public async Task GenericHostProvider_CreateCredentialAsync_NonHttpProtocol_Retu .ReturnsAsync(basicCredential) .Verifiable(); var wiaAuthMock = new Mock(); + var oauthMock = new Mock(); - var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object); + var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object); ICredential credential = await provider.GenerateCredentialAsync(input); @@ -199,8 +202,9 @@ private static async Task TestCreateCredentialAsync_ReturnsEmptyCredential(bool var wiaAuthMock = new Mock(); wiaAuthMock.Setup(x => x.GetIsSupportedAsync(It.IsAny())) .ReturnsAsync(wiaSupported); + var oauthMock = new Mock(); - var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object); + var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object); ICredential credential = await provider.GenerateCredentialAsync(input); @@ -230,8 +234,9 @@ private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(bool var wiaAuthMock = new Mock(); wiaAuthMock.Setup(x => x.GetIsSupportedAsync(It.IsAny())) .ReturnsAsync(wiaSupported); + var oauthMock = new Mock(); - var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object); + var provider = new GenericHostProvider(context, basicAuthMock.Object, wiaAuthMock.Object, oauthMock.Object); ICredential credential = await provider.GenerateCredentialAsync(input); diff --git a/src/shared/Core/Authentication/OAuthAuthentication.cs b/src/shared/Core/Authentication/OAuthAuthentication.cs new file mode 100644 index 000000000..959719dfd --- /dev/null +++ b/src/shared/Core/Authentication/OAuthAuthentication.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GitCredentialManager.Authentication.OAuth; + +namespace GitCredentialManager.Authentication +{ + [Flags] + public enum OAuthAuthenticationModes + { + None = 0, + Browser = 1 << 0, + DeviceCode = 1 << 1, + + All = Browser | DeviceCode + } + + public interface IOAuthAuthentication + { + Task GetAuthenticationModeAsync(string resource, OAuthAuthenticationModes modes); + + Task GetTokenByBrowserAsync(OAuth2Client client, string[] scopes); + + Task GetTokenByDeviceCodeAsync(OAuth2Client client, string[] scopes); + } + + public class OAuthAuthentication : AuthenticationBase, IOAuthAuthentication + { + public OAuthAuthentication(ICommandContext context) + : base (context) { } + + public async Task GetAuthenticationModeAsync( + string resource, OAuthAuthenticationModes modes) + { + EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource)); + + ThrowIfUserInteractionDisabled(); + + // Browser requires a desktop session! + if (!Context.SessionManager.IsDesktopSession) + { + modes &= ~OAuthAuthenticationModes.Browser; + } + + // We need at least one mode! + if (modes == OAuthAuthenticationModes.None) + { + throw new ArgumentException(@$"Must specify at least one {nameof(OAuthAuthenticationModes)}", nameof(modes)); + } + + // If there is no mode choice to be made then just return that result + if (modes == OAuthAuthenticationModes.Browser || + modes == OAuthAuthenticationModes.DeviceCode) + { + return modes; + } + + ThrowIfTerminalPromptsDisabled(); + + switch (modes) + { + case OAuthAuthenticationModes.Browser: + return OAuthAuthenticationModes.Browser; + + case OAuthAuthenticationModes.DeviceCode: + return OAuthAuthenticationModes.DeviceCode; + + default: + var menuTitle = $"Select an authentication method for '{resource}'"; + var menu = new TerminalMenu(Context.Terminal, menuTitle); + + TerminalMenuItem browserItem = null; + TerminalMenuItem deviceItem = null; + + if ((modes & OAuthAuthenticationModes.Browser) != 0) browserItem = menu.Add("Web browser"); + if ((modes & OAuthAuthenticationModes.DeviceCode) != 0) deviceItem = menu.Add("Device code"); + + // Default to the 'first' choice in the menu + TerminalMenuItem choice = menu.Show(0); + + if (choice == browserItem) goto case OAuthAuthenticationModes.Browser; + if (choice == deviceItem) goto case OAuthAuthenticationModes.DeviceCode; + + throw new Exception(); + } + + } + + public async Task GetTokenByBrowserAsync(OAuth2Client client, string[] scopes) + { + ThrowIfUserInteractionDisabled(); + + // We require a desktop session to launch the user's default web browser + if (!Context.SessionManager.IsDesktopSession) + { + throw new InvalidOperationException("Browser authentication requires a desktop session"); + } + + var browserOptions = new OAuth2WebBrowserOptions(); + var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions); + var authCode = await client.GetAuthorizationCodeAsync(scopes, browser, CancellationToken.None); + return await client.GetTokenByAuthorizationCodeAsync(authCode, CancellationToken.None); + } + + public async Task GetTokenByDeviceCodeAsync(OAuth2Client client, string[] scopes) + { + ThrowIfUserInteractionDisabled(); + + OAuth2DeviceCodeResult dcr = await client.GetDeviceCodeAsync(scopes, CancellationToken.None); + + ThrowIfTerminalPromptsDisabled(); + + string deviceMessage = $"To complete authentication please visit {dcr.VerificationUri} and enter the following code:" + + Environment.NewLine + + dcr.UserCode; + Context.Terminal.WriteLine(deviceMessage); + + return await client.GetTokenByDeviceCodeAsync(dcr, CancellationToken.None); + } + } +} diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index 18f288def..845cb3ccc 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using GitCredentialManager.Authentication; +using GitCredentialManager.Authentication.OAuth; namespace GitCredentialManager { @@ -10,20 +13,25 @@ public class GenericHostProvider : HostProvider { private readonly IBasicAuthentication _basicAuth; private readonly IWindowsIntegratedAuthentication _winAuth; + private readonly IOAuthAuthentication _oauth; public GenericHostProvider(ICommandContext context) - : this(context, new BasicAuthentication(context), new WindowsIntegratedAuthentication(context)) { } + : this(context, new BasicAuthentication(context), new WindowsIntegratedAuthentication(context), + new OAuthAuthentication(context)) { } public GenericHostProvider(ICommandContext context, IBasicAuthentication basicAuth, - IWindowsIntegratedAuthentication winAuth) + IWindowsIntegratedAuthentication winAuth, + IOAuthAuthentication oauth) : base(context) { EnsureArgument.NotNull(basicAuth, nameof(basicAuth)); EnsureArgument.NotNull(winAuth, nameof(winAuth)); + EnsureArgument.NotNull(oauth, nameof(oauth)); _basicAuth = basicAuth; _winAuth = winAuth; + _oauth = oauth; } public override string Id => "generic"; @@ -68,7 +76,7 @@ public override async Task GenerateCredentialAsync(InputArguments i Context.Trace.WriteLine($"\tUseAuthHeader = {oauthConfig.UseAuthHeader}"); Context.Trace.WriteLine($"\tDefaultUserName = {oauthConfig.DefaultUserName}"); - throw new NotImplementedException(); + return await GetOAuthAccessToken(uri, input.UserName, oauthConfig); } // Try detecting WIA for this remote, if permitted else if (IsWindowsAuthAllowed) @@ -106,6 +114,65 @@ public override async Task GenerateCredentialAsync(InputArguments i return await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName); } + private async Task GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config) + { + // TODO: Determined user info from a webcall? ID token? Need OIDC support + string oauthUser = userName ?? config.DefaultUserName; + + var client = new OAuth2Client( + HttpClient, + config.Endpoints, + config.ClientId, + config.RedirectUri, + config.ClientSecret, + Context.Trace, + config.UseAuthHeader); + + // Determine which interactive OAuth mode to use. Start by checking for mode preference in config + var supportedModes = OAuthAuthenticationModes.All; + if (Context.Settings.TryGetSetting( + Constants.EnvironmentVariables.OAuthAuthenticationModes, + Constants.GitConfiguration.Credential.SectionName, + Constants.GitConfiguration.Credential.OAuthAuthenticationModes, + out string authModesStr)) + { + if (Enum.TryParse(authModesStr, true, out supportedModes) && supportedModes != OAuthAuthenticationModes.None) + { + Context.Trace.WriteLine($"Supported authentication modes override present: {supportedModes}"); + } + else + { + Context.Trace.WriteLine($"Invalid value for supported authentication modes override setting: '{authModesStr}'"); + } + } + + // If the server doesn't support device code we need to remove it as an option here + if (!config.SupportsDeviceCode) + { + supportedModes &= ~OAuthAuthenticationModes.DeviceCode; + } + + // Prompt the user to select a mode + OAuthAuthenticationModes mode = await _oauth.GetAuthenticationModeAsync(remoteUri.ToString(), supportedModes); + + OAuth2TokenResult tokenResult; + switch (mode) + { + case OAuthAuthenticationModes.Browser: + tokenResult = await _oauth.GetTokenByBrowserAsync(client, config.Scopes); + break; + + case OAuthAuthenticationModes.DeviceCode: + tokenResult = await _oauth.GetTokenByDeviceCodeAsync(client, config.Scopes); + break; + + default: + throw new Exception("No authentication mode selected!"); + } + + return new GitCredential(oauthUser, tokenResult.AccessToken); + } + /// /// Check if the user permits checking for Windows Integrated Authentication. /// @@ -131,9 +198,13 @@ private bool IsWindowsAuthAllowed } } + private HttpClient _httpClient; + private HttpClient HttpClient => _httpClient ?? (_httpClient = Context.HttpClientFactory.CreateClient()); + protected override void ReleaseManagedResources() { _winAuth.Dispose(); + _httpClient?.Dispose(); base.ReleaseManagedResources(); } } From e323c8345762e0082ad2c40d9f5c54b6872050e2 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 27 Jan 2023 11:58:17 -0800 Subject: [PATCH 21/40] generic: add OAuth refresh token support Add support for storing and using OAuth refresh tokens. Prepend "refresh_token" as a subdomain to give better chances of avoiding a name clash compared with appending "/refresh_token" to the path component. --- src/shared/Core/GenericHostProvider.cs | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index 845cb3ccc..2b05537e1 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -128,6 +128,33 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa Context.Trace, config.UseAuthHeader); + // + // Prepend "refresh_token" to the hostname to get a (hopefully) unique service name that + // doesn't clash with an existing credential service. + // + // Appending "/refresh_token" to the end of the remote URI may not always result in a unique + // service because users may set credential.useHttpPath and include "/refresh_token" as a + // path name. + // + string refreshService = new UriBuilder(remoteUri) { Host = $"refresh_token.{remoteUri.Host}" } + .Uri.AbsoluteUri.TrimEnd('/'); + + // Try to use a refresh token if we have one + ICredential refreshToken = Context.CredentialStore.Get(refreshService, userName); + if (refreshToken != null) + { + var refreshResult = await client.GetTokenByRefreshTokenAsync(refreshToken.Password, CancellationToken.None); + + // Store new refresh token if we have been given one + if (!string.IsNullOrWhiteSpace(refreshResult.RefreshToken)) + { + Context.CredentialStore.AddOrUpdate(refreshService, refreshToken.Account, refreshToken.Password); + } + + // Return the new access token + return new GitCredential(oauthUser,refreshResult.AccessToken); + } + // Determine which interactive OAuth mode to use. Start by checking for mode preference in config var supportedModes = OAuthAuthenticationModes.All; if (Context.Settings.TryGetSetting( @@ -170,6 +197,12 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa throw new Exception("No authentication mode selected!"); } + // Store the refresh token if we have one + if (!string.IsNullOrWhiteSpace(tokenResult.RefreshToken)) + { + Context.CredentialStore.AddOrUpdate(refreshService, oauthUser, tokenResult.RefreshToken); + } + return new GitCredential(oauthUser, tokenResult.AccessToken); } From 6702935004094f352c1ef168a500af129d936ecd Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 27 Jan 2023 11:59:24 -0800 Subject: [PATCH 22/40] oauth-ui: Add shared VMs and commands for OAuth Add shared view models and commands for the OAuth GUI prompts. Two new commands and VMs are added, one for the initial 'mode' selection, and another to display the device code. --- .../Core.UI/Commands/DeviceCodeCommand.cs | 52 +++++++++++ src/shared/Core.UI/Commands/OAuthCommand.cs | 93 +++++++++++++++++++ .../Core.UI/ViewModels/DeviceCodeViewModel.cs | 58 ++++++++++++ .../Core.UI/ViewModels/OAuthViewModel.cs | 70 ++++++++++++++ 4 files changed, 273 insertions(+) create mode 100644 src/shared/Core.UI/Commands/DeviceCodeCommand.cs create mode 100644 src/shared/Core.UI/Commands/OAuthCommand.cs create mode 100644 src/shared/Core.UI/ViewModels/DeviceCodeViewModel.cs create mode 100644 src/shared/Core.UI/ViewModels/OAuthViewModel.cs diff --git a/src/shared/Core.UI/Commands/DeviceCodeCommand.cs b/src/shared/Core.UI/Commands/DeviceCodeCommand.cs new file mode 100644 index 000000000..863e24a1d --- /dev/null +++ b/src/shared/Core.UI/Commands/DeviceCodeCommand.cs @@ -0,0 +1,52 @@ +using System; +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 DeviceCodeCommand : HelperCommand + { + protected DeviceCodeCommand(ICommandContext context) + : base(context, "device", "Show device code prompt.") + { + AddOption( + new Option("--code", "User code.") + ); + + AddOption( + new Option("--url", "Verification URL.") + ); + + AddOption( + new Option("--no-logo", "Hide the Git Credential Manager logo and logotype.") + ); + + Handler = CommandHandler.Create(ExecuteAsync); + } + + private async Task ExecuteAsync(string code, string url, bool noLogo) + { + var viewModel = new DeviceCodeViewModel(Context.Environment) + { + UserCode = code, + VerificationUrl = url, + }; + + viewModel.ShowProductHeader = !noLogo; + + await ShowAsync(viewModel, CancellationToken.None); + + if (!viewModel.WindowResult) + { + throw new Exception("User cancelled dialog."); + } + + return 0; + } + + protected abstract Task ShowAsync(DeviceCodeViewModel viewModel, CancellationToken ct); + } +} diff --git a/src/shared/Core.UI/Commands/OAuthCommand.cs b/src/shared/Core.UI/Commands/OAuthCommand.cs new file mode 100644 index 000000000..aa48d7eb1 --- /dev/null +++ b/src/shared/Core.UI/Commands/OAuthCommand.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading; +using System.Threading.Tasks; +using GitCredentialManager.Authentication; +using GitCredentialManager.UI.ViewModels; + +namespace GitCredentialManager.UI.Commands +{ + public abstract class OAuthCommand : HelperCommand + { + protected OAuthCommand(ICommandContext context) + : base(context, "oauth", "Show OAuth authentication prompt.") + { + AddOption( + new Option("--title", "Window title (optional).") + ); + + AddOption( + new Option("--resource", "Resource name or URL (optional).") + ); + + AddOption( + new Option("--browser", "Show browser authentication option.") + ); + + AddOption( + new Option("--device-code", "Show device code authentication option.") + ); + + 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 bool Browser { get; set; } + public bool DeviceCode { get; set; } + public bool NoLogo { get; set; } + } + + private async Task ExecuteAsync(CommandOptions options) + { + var viewModel = new OAuthViewModel(); + + viewModel.Title = !string.IsNullOrWhiteSpace(options.Title) + ? options.Title + : "Git Credential Manager"; + + viewModel.Description = !string.IsNullOrWhiteSpace(options.Resource) + ? $"Sign in to '{options.Resource}'" + : "Select a sign-in option"; + + viewModel.ShowBrowserLogin = options.Browser; + viewModel.ShowDeviceCodeLogin = options.DeviceCode; + viewModel.ShowProductHeader = !options.NoLogo; + + await ShowAsync(viewModel, CancellationToken.None); + + if (!viewModel.WindowResult) + { + throw new Exception("User cancelled dialog."); + } + + var result = new Dictionary(); + switch (viewModel.SelectedMode) + { + case OAuthAuthenticationModes.Browser: + result["mode"] = "browser"; + break; + + case OAuthAuthenticationModes.DeviceCode: + result["mode"] = "devicecode"; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + WriteResult(result); + return 0; + } + + protected abstract Task ShowAsync(OAuthViewModel viewModel, CancellationToken ct); + } +} diff --git a/src/shared/Core.UI/ViewModels/DeviceCodeViewModel.cs b/src/shared/Core.UI/ViewModels/DeviceCodeViewModel.cs new file mode 100644 index 000000000..d44a9f2c7 --- /dev/null +++ b/src/shared/Core.UI/ViewModels/DeviceCodeViewModel.cs @@ -0,0 +1,58 @@ +using System.Windows.Input; + +namespace GitCredentialManager.UI.ViewModels +{ + public class DeviceCodeViewModel : WindowViewModel + { + private readonly IEnvironment _environment; + + private ICommand _verificationUrlCommand; + private string _verificationUrl; + private string _userCode; + private bool _showProductHeader; + + public DeviceCodeViewModel() + { + // Constructor the XAML designer + } + + public DeviceCodeViewModel(IEnvironment environment) + { + EnsureArgument.NotNull(environment, nameof(environment)); + + _environment = environment; + + Title = "Device code authentication"; + VerificationUrlCommand = new RelayCommand(OpenVerificationUrl); + } + + private void OpenVerificationUrl() + { + BrowserUtils.OpenDefaultBrowser(_environment, VerificationUrl); + } + + public string UserCode + { + get => _userCode; + set => SetAndRaisePropertyChanged(ref _userCode, value); + } + + public string VerificationUrl + { + get => _verificationUrl; + set => SetAndRaisePropertyChanged(ref _verificationUrl, value); + } + + public ICommand VerificationUrlCommand + { + get => _verificationUrlCommand; + set => SetAndRaisePropertyChanged(ref _verificationUrlCommand, value); + } + + public bool ShowProductHeader + { + get => _showProductHeader; + set => SetAndRaisePropertyChanged(ref _showProductHeader, value); + } + } +} diff --git a/src/shared/Core.UI/ViewModels/OAuthViewModel.cs b/src/shared/Core.UI/ViewModels/OAuthViewModel.cs new file mode 100644 index 000000000..607642f31 --- /dev/null +++ b/src/shared/Core.UI/ViewModels/OAuthViewModel.cs @@ -0,0 +1,70 @@ +using GitCredentialManager.Authentication; + +namespace GitCredentialManager.UI.ViewModels +{ + public class OAuthViewModel : WindowViewModel + { + private string _description; + private bool _showProductHeader; + private bool _showBrowserLogin; + private bool _showDeviceCodeLogin; + private RelayCommand _signInBrowserCommand; + private RelayCommand _signInDeviceCodeCommand; + + public OAuthViewModel() + { + SignInBrowserCommand = new RelayCommand(SignInWithBrowser); + SignInDeviceCodeCommand = new RelayCommand(SignInWithDeviceCode); + } + + private void SignInWithBrowser() + { + SelectedMode = OAuthAuthenticationModes.Browser; + Accept(); + } + + private void SignInWithDeviceCode() + { + SelectedMode = OAuthAuthenticationModes.DeviceCode; + Accept(); + } + + public string Description + { + get => _description; + set => SetAndRaisePropertyChanged(ref _description, value); + } + + public bool ShowProductHeader + { + get => _showProductHeader; + set => SetAndRaisePropertyChanged(ref _showProductHeader, value); + } + + public bool ShowBrowserLogin + { + get => _showBrowserLogin; + set => SetAndRaisePropertyChanged(ref _showBrowserLogin, value); + } + + public bool ShowDeviceCodeLogin + { + get => _showDeviceCodeLogin; + set => SetAndRaisePropertyChanged(ref _showDeviceCodeLogin, value); + } + + public RelayCommand SignInBrowserCommand + { + get => _signInBrowserCommand; + set => SetAndRaisePropertyChanged(ref _signInBrowserCommand, value); + } + + public RelayCommand SignInDeviceCodeCommand + { + get => _signInDeviceCodeCommand; + set => SetAndRaisePropertyChanged(ref _signInDeviceCodeCommand, value); + } + + public OAuthAuthenticationModes SelectedMode { get; private set; } + } +} From d59cd44768cbdc5956e7a3e2b2010214c0dd6efa Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 27 Jan 2023 12:16:25 -0800 Subject: [PATCH 23/40] generic-ui: add Avalonia impl of OAuth and Device Code Add AvaloniaUI based implementations of the OAuth and Device Code generic UI prompts. --- .../Commands/DeviceCodeCommandImpl.cs | 17 ++++++ .../Commands/OAuthCommandImpl.cs | 17 ++++++ .../Controls/TesterWindow.axaml | 49 +++++++++++++++++ .../Controls/TesterWindow.axaml.cs | 54 +++++++++++++++++++ .../Git-Credential-Manager.UI.Avalonia.csproj | 8 +++ .../Program.cs | 2 + .../Views/DeviceCodeView.axaml | 44 +++++++++++++++ .../Views/DeviceCodeView.axaml.cs | 18 +++++++ .../Views/OAuthView.axaml | 49 +++++++++++++++++ .../Views/OAuthView.axaml.cs | 18 +++++++ 10 files changed, 276 insertions(+) create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Commands/DeviceCodeCommandImpl.cs create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Commands/OAuthCommandImpl.cs create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Views/DeviceCodeView.axaml create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Views/DeviceCodeView.axaml.cs create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Views/OAuthView.axaml create mode 100644 src/shared/Git-Credential-Manager.UI.Avalonia/Views/OAuthView.axaml.cs diff --git a/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/DeviceCodeCommandImpl.cs b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/DeviceCodeCommandImpl.cs new file mode 100644 index 000000000..045dcab15 --- /dev/null +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/DeviceCodeCommandImpl.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 DeviceCodeCommandImpl : DeviceCodeCommand + { + public DeviceCodeCommandImpl(ICommandContext context) : base(context) { } + + protected override Task ShowAsync(DeviceCodeViewModel viewModel, CancellationToken ct) + { + return AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), ct); + } + } +} diff --git a/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/OAuthCommandImpl.cs b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/OAuthCommandImpl.cs new file mode 100644 index 000000000..536cda52a --- /dev/null +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Commands/OAuthCommandImpl.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 OAuthCommandImpl : OAuthCommand + { + public OAuthCommandImpl(ICommandContext context) : base(context) { } + + protected override Task ShowAsync(OAuthViewModel 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 index e10e5c401..4f96254d6 100644 --- a/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Controls/TesterWindow.axaml @@ -45,5 +45,54 @@