diff --git a/.azure-pipelines/continuous-integration.yml b/.azure-pipelines/continuous-integration.yml deleted file mode 100644 index cd9b304a0..000000000 --- a/.azure-pipelines/continuous-integration.yml +++ /dev/null @@ -1,32 +0,0 @@ -trigger: - - main - -variables: - configuration: Release - winImage: vs2017-win2016 - osxImage: macos-latest - -jobs: -- job: windows - displayName: Windows - pool: - vmImage: $(winImage) - steps: - - template: templates/windows/compile.yml - - template: templates/windows/pack.yml - -- job: osx - displayName: macOS - pool: - vmImage: $(osxImage) - steps: - - template: templates/osx/compile.yml - - template: templates/osx/pack.unsigned.yml - -- job: ubuntu1804_x86_64 - displayName: Ubuntu 18.04 LTS x86_64 - pool: - vmImage: ubuntu-18.04 - steps: - - template: templates/linux/compile.yml - - template: templates/linux/pack.unsigned.yml diff --git a/.azure-pipelines/pull-request.yml b/.azure-pipelines/pull-request.yml deleted file mode 100644 index e24d3d6e1..000000000 --- a/.azure-pipelines/pull-request.yml +++ /dev/null @@ -1,33 +0,0 @@ -pr: - - main - - release - -variables: - configuration: Release - winImage: vs2017-win2016 - osxImage: macos-latest - -jobs: -- job: windows - displayName: Windows - pool: - vmImage: $(winImage) - steps: - - template: templates/windows/compile.yml - - template: templates/windows/pack.yml - -- job: osx - displayName: macOS - pool: - vmImage: $(osxImage) - steps: - - template: templates/osx/compile.yml - - template: templates/osx/pack.unsigned.yml - -- job: ubuntu1804_x86_64 - displayName: Ubuntu 18.04 LTS x86_64 - pool: - vmImage: ubuntu-18.04 - steps: - - template: templates/linux/compile.yml - - template: templates/linux/pack.unsigned.yml diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml deleted file mode 100644 index 934942932..000000000 --- a/.azure-pipelines/release.yml +++ /dev/null @@ -1,69 +0,0 @@ -trigger: - - release - -variables: - configuration: Release - signPool: MSEngSS-MicroBuild2019-1ES - winImage: vs2017-win2016 - osxImage: macos-latest - -jobs: -- job: windows - displayName: Windows - pool: - name: $(signPool) - steps: - - template: templates/windows/compile.signed.yml - - template: templates/windows/pack.yml - -- job: osx_step1 - displayName: macOS (Build & Layout) - pool: - vmImage: $(osxImage) - steps: - - template: templates/osx/compile.yml - - template: templates/osx/pack.signed/step1-layout.yml - -- job: osx_step2 - displayName: macOS (Sign payload) - dependsOn: osx_step1 - condition: succeeded() - pool: - name: $(signPool) - steps: - - template: templates/osx/pack.signed/step2-signpayload.yml - -- job: osx_step3 - displayName: macOS (Pack) - dependsOn: osx_step2 - condition: succeeded() - pool: - vmImage: $(osxImage) - steps: - - template: templates/osx/pack.signed/step3-pack.yml - -- job: osx_step4 - displayName: macOS (Sign package) - dependsOn: osx_step3 - condition: succeeded() - pool: - name: $(signPool) - steps: - - template: templates/osx/pack.signed/step4-signpack.yml - -- job: osx_step5 - displayName: macOS (Prepare for distribution) - dependsOn: osx_step4 - condition: succeeded() - pool: - vmImage: $(osxImage) - steps: - - template: templates/osx/pack.signed/step5-dist.yml - -- job: ubuntu1804_x86_64 - displayName: Ubuntu 18.04 LTS x86_64 - pool: - vmImage: ubuntu-18.04 - steps: - - template: templates/linux/compile.yml - - template: templates/linux/pack.unsigned.yml diff --git a/.azure-pipelines/templates/linux/compile.yml b/.azure-pipelines/templates/linux/compile.yml deleted file mode 100644 index a45ab6b30..000000000 --- a/.azure-pipelines/templates/linux/compile.yml +++ /dev/null @@ -1,22 +0,0 @@ -steps: - - task: UseDotNet@2 - displayName: Use .NET SDK 6.0.201 - inputs: - packageType: sdk - version: 6.0.201 - - - task: DotNetCoreCLI@2 - displayName: Compile common code - inputs: - command: build - projects: 'Git-Credential-Manager.sln' - arguments: '--configuration=Linux$(configuration)' - - - task: DotNetCoreCLI@2 - displayName: Run common unit tests - inputs: - command: test - projects: 'Git-Credential-Manager.sln' - arguments: '--configuration=Linux$(configuration)' - publishTestResults: true - testRunTitle: 'Unit tests (Linux)' diff --git a/.azure-pipelines/templates/linux/pack.unsigned.yml b/.azure-pipelines/templates/linux/pack.unsigned.yml deleted file mode 100644 index 08f9ccffd..000000000 --- a/.azure-pipelines/templates/linux/pack.unsigned.yml +++ /dev/null @@ -1,12 +0,0 @@ -steps: - - script: | - mkdir -p "$(Build.StagingDirectory)/publish/" - cp "out/linux/Packaging.Linux/tar/$(configuration)/"*.tar.gz "$(Build.StagingDirectory)/publish/" - cp "out/linux/Packaging.Linux/deb/$(configuration)/"*.deb "$(Build.StagingDirectory)/publish/" - displayName: Prepare final build artifacts - - - task: PublishPipelineArtifact@0 - displayName: Publish unsigned installer artifacts - inputs: - artifactName: 'Installer.Linux.Unsigned' - targetPath: '$(Build.StagingDirectory)/publish' diff --git a/.azure-pipelines/templates/osx/compile.yml b/.azure-pipelines/templates/osx/compile.yml deleted file mode 100644 index d480a9213..000000000 --- a/.azure-pipelines/templates/osx/compile.yml +++ /dev/null @@ -1,22 +0,0 @@ -steps: - - task: UseDotNet@2 - displayName: Use .NET SDK 6.0.201 - inputs: - packageType: sdk - version: 6.0.201 - - - task: DotNetCoreCLI@2 - displayName: Compile common code and macOS Helpers - inputs: - command: build - projects: 'Git-Credential-Manager.sln' - arguments: '--configuration=Mac$(configuration)' - - - task: DotNetCoreCLI@2 - displayName: Run common unit tests - inputs: - command: test - projects: 'Git-Credential-Manager.sln' - arguments: '--configuration=Mac$(configuration)' - publishTestResults: true - testRunTitle: 'Unit tests (macOS)' diff --git a/.azure-pipelines/templates/osx/pack.signed/step1-layout.yml b/.azure-pipelines/templates/osx/pack.signed/step1-layout.yml deleted file mode 100644 index cb93812b6..000000000 --- a/.azure-pipelines/templates/osx/pack.signed/step1-layout.yml +++ /dev/null @@ -1,15 +0,0 @@ -steps: - - script: src/osx/Installer.Mac/layout.sh --configuration='$(configuration)' --output='$(Build.StagingDirectory)/payload' --symbol-output='$(Build.StagingDirectory)/symbols' - displayName: Layout installer payload - - - task: PublishPipelineArtifact@0 - displayName: Upload unsigned payload - inputs: - artifactName: 'tmp.macpayload_unsigned' - targetPath: '$(Build.StagingDirectory)/payload' - - - task: PublishPipelineArtifact@0 - displayName: Upload symbols - inputs: - artifactName: 'tmp.macsymbols' - targetPath: '$(Build.StagingDirectory)/symbols' diff --git a/.azure-pipelines/templates/osx/pack.signed/step2-signpayload.yml b/.azure-pipelines/templates/osx/pack.signed/step2-signpayload.yml deleted file mode 100644 index 01439a347..000000000 --- a/.azure-pipelines/templates/osx/pack.signed/step2-signpayload.yml +++ /dev/null @@ -1,48 +0,0 @@ -steps: - - task: NuGetAuthenticate@0 - displayName: Authenticate to MicroBuild NuGet feed - inputs: - nuGetServiceConnections: 'MicroBuild Toolset Nuget Feed (Read)' - - - task: ms-vseng.MicroBuildTasks.30666190-6959-11e5-9f96-f56098202fef.MicroBuildSigningPlugin@3 - displayName: Install signing plugin - inputs: - signType: '$(SignType)' - - - task: DownloadPipelineArtifact@1 - displayName: Download unsigned payload - inputs: - buildType: 'current' - artifactName: 'tmp.macpayload_unsigned' - downloadPath: '$(Build.StagingDirectory)\payload' - - - task: UseDotNet@2 - displayName: Use .NET SDK 6.0.201 - inputs: - packageType: sdk - version: 6.0.201 - - - task: NuGetToolInstaller@0 - displayName: Install NuGet tool >=4.3.0 - inputs: - versionSpec: '>=4.3.0' - - # Must use the NuGet & MSBuild toolchain here rather than `dotnet` - # because the signing tasks target the netfx MSBuild runtime only. - - task: NuGetCommand@2 - displayName: Restore MicroBuild packages - inputs: - command: restore - restoreSolution: 'src\osx\SignFiles.Mac\SignFiles.Mac.csproj' - - - task: MSBuild@1 - displayName: Sign payload - inputs: - solution: 'src\osx\SignFiles.Mac\SignFiles.Mac.csproj' - msbuildArguments: '/p:RootDir="$(Build.StagingDirectory)\payload"' - - - task: PublishPipelineArtifact@0 - displayName: Upload signed payload - inputs: - artifactName: 'tmp.macpayload_signed' - targetPath: '$(Build.StagingDirectory)\payload' diff --git a/.azure-pipelines/templates/osx/pack.signed/step3-pack.yml b/.azure-pipelines/templates/osx/pack.signed/step3-pack.yml deleted file mode 100644 index 897aca172..000000000 --- a/.azure-pipelines/templates/osx/pack.signed/step3-pack.yml +++ /dev/null @@ -1,31 +0,0 @@ -steps: - - task: DownloadPipelineArtifact@1 - displayName: Download signed payload - inputs: - buildType: 'current' - artifactName: 'tmp.macpayload_signed' - downloadPath: '$(Build.StagingDirectory)/payload' - - - task: UseDotNet@2 - displayName: Use .NET SDK 6.0.201 - inputs: - packageType: sdk - version: 6.0.201 - - - script: dotnet tool install --global nbgv - displayName: Install Nerdbank.GitVersioning tool - - - script: nbgv cloud --common-vars - displayName: Set version variables - - - script: src/osx/Installer.Mac/pack.sh --payload='$(Build.StagingDirectory)/payload' --version='$(GitBuildVersionSimple)' --output='$(Build.StagingDirectory)/components/com.microsoft.gitcredentialmanager.component.pkg' - displayName: Create component package - - - script: src/osx/Installer.Mac/dist.sh --package-path='$(Build.StagingDirectory)/components' --version='$(GitBuildVersionSimple)' --output='$(Build.StagingDirectory)/pkg/gcmcore-osx-$(GitBuildVersionSimple).pkg' || exit 1 - displayName: Create product archive - - - task: PublishPipelineArtifact@0 - displayName: Upload unsigned package - inputs: - artifactName: 'tmp.macinstaller_unsigned' - targetPath: '$(Build.StagingDirectory)/pkg/gcmcore-osx-$(GitBuildVersionSimple).pkg' diff --git a/.azure-pipelines/templates/osx/pack.signed/step4-signpack.yml b/.azure-pipelines/templates/osx/pack.signed/step4-signpack.yml deleted file mode 100644 index 0a7e05d6d..000000000 --- a/.azure-pipelines/templates/osx/pack.signed/step4-signpack.yml +++ /dev/null @@ -1,42 +0,0 @@ -steps: - - task: NuGetAuthenticate@0 - displayName: Authenticate to MicroBuild NuGet feed - inputs: - nuGetServiceConnections: 'MicroBuild Toolset Nuget Feed (Read)' - - - task: ms-vseng.MicroBuildTasks.30666190-6959-11e5-9f96-f56098202fef.MicroBuildSigningPlugin@3 - displayName: Install signing plugin - inputs: - signType: '$(SignType)' - - - task: DownloadPipelineArtifact@1 - displayName: Download unsigned package - inputs: - buildType: 'current' - artifactName: 'tmp.macinstaller_unsigned' - downloadPath: '$(Build.StagingDirectory)\pkg' - - - powershell: | - $dir="$(Build.StagingDirectory)\pkg" - Compress-Archive -Path $dir\*.pkg $dir\gcmcorepkg.zip - Remove-Item $dir\*.pkg - displayName: 'Zip package file for signing' - - - task: ms-vseng.MicroBuildTasks.7973a23b-33e3-4b00-a7d9-c06d90f8297f.MicroBuildSignMacFiles@1 - displayName: Sign package - inputs: - SigningTarget: '$(Build.StagingDirectory)\pkg\gcmcorepkg.zip' - SigningCert: 8003 - condition: and(succeeded(), ne(variables['SignType'], 'test')) - - - powershell: | - $dir="$(Build.StagingDirectory)\pkg" - Expand-Archive -LiteralPath $dir\gcmcorepkg.zip -DestinationPath $dir -Force - Remove-Item $dir\gcmcorepkg.zip -Force - displayName: 'Unzip signed package file' - - - task: PublishPipelineArtifact@0 - displayName: Upload signed installer - inputs: - artifactName: 'tmp.macinstaller_signed' - targetPath: '$(Build.StagingDirectory)\pkg' diff --git a/.azure-pipelines/templates/osx/pack.signed/step5-dist.yml b/.azure-pipelines/templates/osx/pack.signed/step5-dist.yml deleted file mode 100644 index 31a4cbcc7..000000000 --- a/.azure-pipelines/templates/osx/pack.signed/step5-dist.yml +++ /dev/null @@ -1,63 +0,0 @@ -steps: - - task: DownloadPipelineArtifact@1 - displayName: Download signed installer - inputs: - buildType: 'current' - artifactName: 'tmp.macinstaller_signed' - downloadPath: '$(Build.StagingDirectory)/pkg' - - - task: DownloadPipelineArtifact@1 - displayName: Download signed payload - inputs: - buildType: 'current' - artifactName: 'tmp.macpayload_signed' - downloadPath: '$(Build.StagingDirectory)/payload' - - - task: DownloadPipelineArtifact@1 - displayName: Download symbols - inputs: - buildType: 'current' - artifactName: 'tmp.macsymbols' - downloadPath: '$(Build.StagingDirectory)/symbols' - - # Skip notarization until we can preserve the hardened runtime bit and sign the .NET runtime bits - # Tracked: https://github.com/microsoft/Git-Credential-Manager-Core/issues/108 - #- script: src/osx/SignFiles.Mac/notarize-pkg.sh -id "$(AppleId)" -p "$(AppleIdPassword)" -pkg "$(Build.StagingDirectory)"/pkg/*.pkg - # displayName: Notarize and staple installer package - - - script: | - mkdir -p "$(Build.StagingDirectory)/publish/payload" "$(Build.StagingDirectory)/publish/payload.sym" - cp -f "$(Build.StagingDirectory)"/pkg/*.pkg "$(Build.StagingDirectory)/publish/" - cp -Rf "$(Build.StagingDirectory)/payload/" "$(Build.StagingDirectory)/publish/payload/" - cp -Rf "$(Build.StagingDirectory)/symbols/" "$(Build.StagingDirectory)/publish/payload.sym/" - displayName: Prepare final build artifacts - - - script: dotnet tool install --global nbgv - displayName: Install Nerdbank.GitVersioning tool - - - script: nbgv cloud --common-vars - displayName: Set version variables - - - task: ArchiveFiles@2 - displayName: Create payload archive - inputs: - rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload/' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.StagingDirectory)/publish/gcmcore-osx-$(GitBuildVersionSimple).tar.gz' - replaceExistingArchive: true - - - task: ArchiveFiles@2 - displayName: Create symbol archive - inputs: - rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload.sym/' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.StagingDirectory)/publish/symbols-osx.tar.gz' - replaceExistingArchive: true - - - task: PublishPipelineArtifact@0 - displayName: Publish signed installer artifacts - inputs: - artifactName: 'Installer.Mac.Signed' - targetPath: '$(Build.StagingDirectory)/publish' diff --git a/.azure-pipelines/templates/osx/pack.unsigned.yml b/.azure-pipelines/templates/osx/pack.unsigned.yml deleted file mode 100644 index e225d31ef..000000000 --- a/.azure-pipelines/templates/osx/pack.unsigned.yml +++ /dev/null @@ -1,34 +0,0 @@ -steps: - - script: | - cp -R "out/osx/Installer.Mac/pkg/$(configuration)" "$(Build.StagingDirectory)/publish/" - displayName: Prepare final build artifacts - - - script: dotnet tool install --global nbgv - displayName: Install Nerdbank.GitVersioning tool - - - script: nbgv cloud --common-vars - displayName: Set version variables - - - task: ArchiveFiles@2 - displayName: Create payload archive - inputs: - rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.StagingDirectory)/publish/gcmcore-osx-$(GitBuildVersionSimple).tar.gz' - replaceExistingArchive: true - - - task: ArchiveFiles@2 - displayName: Create symbol archive - inputs: - rootFolderOrFile: '$(Build.StagingDirectory)/publish/payload.sym/' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.StagingDirectory)/publish/symbols-osx.tar.gz' - replaceExistingArchive: true - - - task: PublishPipelineArtifact@0 - displayName: Publish unsigned installer artifacts - inputs: - artifactName: 'Installer.Mac.Unsigned' - targetPath: '$(Build.StagingDirectory)/publish' diff --git a/.azure-pipelines/templates/windows/compile.signed.yml b/.azure-pipelines/templates/windows/compile.signed.yml deleted file mode 100644 index ab2afefaf..000000000 --- a/.azure-pipelines/templates/windows/compile.signed.yml +++ /dev/null @@ -1,46 +0,0 @@ -steps: - - task: NuGetAuthenticate@0 - displayName: Authenticate to MicroBuild NuGet feed - inputs: - nuGetServiceConnections: 'MicroBuild Toolset Nuget Feed (Read)' - - - task: ms-vseng.MicroBuildTasks.30666190-6959-11e5-9f96-f56098202fef.MicroBuildSigningPlugin@3 - displayName: Install signing plugin - condition: and(succeeded(), eq(variables['SignType'], 'real')) - inputs: - signType: '$(SignType)' - - - task: UseDotNet@2 - displayName: Use .NET SDK 6.0.201 - inputs: - packageType: sdk - version: 6.0.201 - - - task: NuGetToolInstaller@0 - displayName: Install NuGet tool >=4.3.0 - inputs: - versionSpec: '>=4.3.0' - - # Must use the NuGet & MSBuild toolchain here rather than `dotnet` - # because the signing tasks target the netfx MSBuild runtime only. - - task: NuGetCommand@2 - displayName: Restore packages - inputs: - command: restore - restoreSolution: 'Git-Credential-Manager.sln' - configuration: 'Windows$(configuration)' - - - task: MSBuild@1 - displayName: Compile common code and Windows helpers - inputs: - solution: 'Git-Credential-Manager.sln' - configuration: 'Windows$(configuration)' - - - task: VSTest@2 - displayName: Run common unit tests - inputs: - testAssemblyVer2: | - out\shared\*.Tests\bin\**\*.Tests.dll - configuration: 'Windows$(configuration)' - otherConsoleOptions: '/Framework:.NETCoreApp,Version=2.1' - testRunTitle: 'Unit tests (Windows)' diff --git a/.azure-pipelines/templates/windows/compile.yml b/.azure-pipelines/templates/windows/compile.yml deleted file mode 100644 index 48d664869..000000000 --- a/.azure-pipelines/templates/windows/compile.yml +++ /dev/null @@ -1,29 +0,0 @@ -steps: - - task: UseDotNet@2 - displayName: Use .NET SDK 6.0.201 - inputs: - packageType: sdk - version: 6.0.201 - - - task: DotNetCoreCLI@2 - displayName: Restore packages - inputs: - command: restore - projects: 'Git-Credential-Manager.sln' - arguments: '--configuration=Windows$(configuration)' - - - task: DotNetCoreCLI@2 - displayName: Compile common code and Windows Helpers - inputs: - command: build - projects: 'Git-Credential-Manager.sln' - arguments: '--configuration=Windows$(configuration)' - - - task: DotNetCoreCLI@2 - displayName: Run common unit tests - inputs: - command: test - projects: 'Git-Credential-Manager.sln' - arguments: '--configuration=Mac$(configuration)' - publishTestResults: true - testRunTitle: 'Unit tests (Windows)' diff --git a/.azure-pipelines/templates/windows/pack.yml b/.azure-pipelines/templates/windows/pack.yml deleted file mode 100644 index c6e6fde63..000000000 --- a/.azure-pipelines/templates/windows/pack.yml +++ /dev/null @@ -1,45 +0,0 @@ -steps: - - script: dotnet tool install --tool-path .tools nbgv - displayName: Install Nerdbank.GitVersioning tool - - - script: .tools\nbgv cloud --common-vars - displayName: Set version variables - - - script: | - xcopy "out\windows\Installer.Windows\bin\$(configuration)\net472" "$(Build.StagingDirectory)\publish\" - xcopy "out\windows\Payload.Windows\bin\$(configuration)\net472\win-x86" "$(Build.StagingDirectory)\publish\payload\" - mkdir "$(Build.StagingDirectory)\publish\payload.sym\" - move "$(Build.StagingDirectory)\publish\payload\*.pdb" "$(Build.StagingDirectory)\publish\payload.sym\" - displayName: Prepare final build artifacts - - - task: ArchiveFiles@2 - displayName: Create payload archive - inputs: - rootFolderOrFile: '$(Build.StagingDirectory)\publish\payload\' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.StagingDirectory)\publish\gcmcore-win-x86-$(GitBuildVersionSimple).zip' - replaceExistingArchive: true - - - task: ArchiveFiles@2 - displayName: Create symbol archive - inputs: - rootFolderOrFile: '$(Build.StagingDirectory)\publish\payload.sym\' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.StagingDirectory)\publish\symbols-win-x86.zip' - replaceExistingArchive: true - - - task: PublishPipelineArtifact@0 - displayName: Publish unsigned installer artifacts - condition: and(succeeded(), ne(variables['SignType'], 'real')) - inputs: - artifactName: 'Installer.Windows.Unsigned' - targetPath: '$(Build.StagingDirectory)\publish' - - - task: PublishPipelineArtifact@0 - displayName: Publish signed installer artifacts - condition: and(succeeded(), eq(variables['SignType'], 'real')) - inputs: - artifactName: 'Installer.Windows.Signed' - targetPath: '$(Build.StagingDirectory)\publish' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..b0f8d194a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 + +updates: + + # Enable version updates for GitHub ecosystem + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/run_developer_signing.sh b/.github/run_developer_signing.sh new file mode 100755 index 000000000..8b3de88a3 --- /dev/null +++ b/.github/run_developer_signing.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +SIGN_DIR=$1 +DEVELOPER_ID=$2 +ENTITLEMENTS_FILE=$3 + +if [ -z "$SIGN_DIR" ]; then + echo "error: missing directory argument" + exit 1 +elif [ -z "$DEVELOPER_ID" ]; then + echo "error: missing developer id argument" + exit 1 +elif [ -z "$ENTITLEMENTS_FILE" ]; then + echo "error: missing entitlements file argument" + exit 1 +fi + +echo "======== INPUTS ========" +echo "Directory: $SIGN_DIR" +echo "Developer ID: $DEVELOPER_ID" +echo "Entitlements: $ENTITLEMENTS_FILE" +echo "======== END INPUTS ========" + +cd $SIGN_DIR +for f in * +do + macho=$(file --mime $f | grep mach) + # Runtime sign dylibs and Mach-O binaries + if [[ $f == *.dylib ]] || [ ! -z "$macho" ]; + then + echo "Runtime Signing $f" + codesign -s "$DEVELOPER_ID" $f --timestamp --force --options=runtime --entitlements $ENTITLEMENTS_FILE + elif [ -d "$f" ]; + then + echo "Signing files in subdirectory $f" + cd $f + for i in * + do + codesign -s "$DEVELOPER_ID" $i --timestamp --force + done + cd .. + else + echo "Signing $f" + codesign -s "$DEVELOPER_ID" $f --timestamp --force + fi +done \ No newline at end of file diff --git a/.github/run_esrp_signing.py b/.github/run_esrp_signing.py index fd137976e..dc598fdcd 100644 --- a/.github/run_esrp_signing.py +++ b/.github/run_esrp_signing.py @@ -1,3 +1,4 @@ +import argparse import json import os import glob @@ -6,40 +7,44 @@ import sys import re +parser = argparse.ArgumentParser(description='Sign binaries for Windows, macOS, and Linux') +parser.add_argument('path', help='Path to file for signing') +parser.add_argument('keycode', help='Platform-specific key code for signing') +parser.add_argument('opcode', help='Platform-specific operation code for signing') +# Setting nargs=argparse.REMAINDER allows us to pass in params that begin with `--` +parser.add_argument('--params', nargs=argparse.REMAINDER, help='Parameters for signing') +args = parser.parse_args() + esrp_tool = os.path.join("esrp", "tools", "EsrpClient.exe") aad_id = os.environ['AZURE_AAD_ID'].strip() +# We temporarily need two AAD IDs, as we're using an SSL certificate associated +# with an older App Registration until we have the required hardware to approve +# the new certificate in SSL Admin. +aad_id_ssl = os.environ['AZURE_AAD_ID_SSL'].strip() workspace = os.environ['GITHUB_WORKSPACE'].strip() -source_root_location = os.path.join(workspace, "deb", "Release") -destination_location = os.path.join(workspace) - -files = glob.glob(os.path.join(source_root_location, "*.deb")) +source_location = args.path +files = glob.glob(os.path.join(source_location, "*")) print("Found files:") pprint.pp(files) -if len(files) < 1 or not files[0].endswith(".deb"): - print("Error: cannot find .deb to sign") - exit(1) - -file_to_sign = os.path.basename(files[0]) - auth_json = { - "Version": "1.0.0", - "AuthenticationType": "AAD_CERT", - "TenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47", - "ClientId": aad_id, - "AuthCert": { - "SubjectName": f"CN={aad_id}.microsoft.com", - "StoreLocation": "LocalMachine", - "StoreName": "My", - }, - "RequestSigningCert": { - "SubjectName": f"CN={aad_id}", - "StoreLocation": "LocalMachine", - "StoreName": "My", - } + "Version": "1.0.0", + "AuthenticationType": "AAD_CERT", + "TenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47", + "ClientId": f"{aad_id}", + "AuthCert": { + "SubjectName": f"CN={aad_id_ssl}.microsoft.com", + "StoreLocation": "LocalMachine", + "StoreName": "My" + }, + "RequestSigningCert": { + "SubjectName": f"CN={aad_id}", + "StoreLocation": "LocalMachine", + "StoreName": "My" + } } input_json = { @@ -47,21 +52,15 @@ "SignBatches": [ { "SourceLocationType": "UNC", - "SourceRootDirectory": source_root_location, + "SourceRootDirectory": source_location, "DestinationLocationType": "UNC", - "DestinationRootDirectory": destination_location, - "SignRequestFiles": [ - { - "CustomerCorrelationId": "01A7F55F-6CDD-4123-B255-77E6F212CDAD", - "SourceLocation": file_to_sign, - "DestinationLocation": os.path.join("Signed", file_to_sign), - } - ], + "DestinationRootDirectory": workspace, + "SignRequestFiles": [], "SigningInfo": { "Operations": [ { - "KeyCode": "CP-450779-Pgp", - "OperationCode": "LinuxSign", + "KeyCode": f"{args.keycode}", + "OperationCode": f"{args.opcode}", "Parameters": {}, "ToolName": "sign", "ToolVersion": "1.0", @@ -72,10 +71,27 @@ ] } +# add files to sign +for f in files: + name = os.path.basename(f) + input_json["SignBatches"][0]["SignRequestFiles"].append( + { + "SourceLocation": name, + "DestinationLocation": os.path.join("signed", name), + } + ) + +# add parameters to input.json (e.g. enabling the hardened runtime for macOS) +if args.params is not None: + i = 0 + while i < len(args.params): + input_json["SignBatches"][0]["SigningInfo"]["Operations"][0]["Parameters"][args.params[i]] = args.params[i + 1] + i += 2 + policy_json = { "Version": "1.0.0", "Intent": "production release", - "ContentType": "Debian package", + "ContentType": "binary", } configs = [ @@ -106,7 +122,7 @@ '***', result.stdout, flags=re.IGNORECASE|re.MULTILINE) -printf(log) +print(log) if result.returncode != 0: print("Failed to run ESRPClient.exe") @@ -117,6 +133,6 @@ with open(esrp_out, 'r') as fp: pprint.pp(json.load(fp)) -signed_file = os.path.join(destination_location, "Signed", file_to_sign) -if os.path.isfile(signed_file): - print(f"Success!\nSigned {signed_file}") +for file in files: + if os.path.isfile(os.path.join("signed", file)): + print(f"Success!\nSigned {file}") \ No newline at end of file diff --git a/.github/set_up_esrp.ps1 b/.github/set_up_esrp.ps1 new file mode 100644 index 000000000..099cd50c2 --- /dev/null +++ b/.github/set_up_esrp.ps1 @@ -0,0 +1,12 @@ +# Install ESRP client +az storage blob download --file esrp.zip --account-key "$env:AZURE_STORAGE_KEY" --account-name gcmesrp --container microsoft-esrp-client --name microsoft.esrpclient.1.2.76.nupkg +Expand-Archive -Path esrp.zip -DestinationPath .\esrp + +# Install certificates +az keyvault secret download --vault-name "$env:AZURE_VAULT" --name "$env:AUTH_CERT" --file out.pfx +certutil -f -importpfx out.pfx +Remove-Item out.pfx + +az keyvault secret download --vault-name "$env:AZURE_VAULT" --name "$env:REQUEST_SIGNING_CERT" --file out.pfx +certutil -f -importpfx out.pfx +Remove-Item out.pfx \ No newline at end of file diff --git a/.github/workflows/build-installers.yml b/.github/workflows/build-installers.yml deleted file mode 100644 index ebd8b1c8a..000000000 --- a/.github/workflows/build-installers.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Build-Installers - -on: - workflow_dispatch: - push: - branches: [ main, release ] - pull_request: - branches: [ main ] - -jobs: - linux: - name: "Linux" - - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. - - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.201 - - - name: Install dependencies - run: dotnet restore --force - - - name: Build Linux Payloads - run: dotnet build -c Release src/linux/Packaging.Linux/Packaging.Linux.csproj - - - name: Upload Installers - uses: actions/upload-artifact@v2 - with: - name: Installers - path: | - out/linux/Packaging.Linux/deb/Release/*.deb - out/linux/Packaging.Linux/tar/Release/*.tar.gz diff --git a/.github/workflows/build-signed-deb.yml b/.github/workflows/build-signed-deb.yml deleted file mode 100644 index 6437225d1..000000000 --- a/.github/workflows/build-signed-deb.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: "Build Signed Debian Installer" - -on: - workflow_dispatch: - release: - types: [released] - -jobs: - build: - name: "Build" - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. - - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.201 - - - name: Install dependencies - run: dotnet restore --force - - - name: Build Linux Payloads - run: dotnet build -c Release src/linux/Packaging.Linux/Packaging.Linux.csproj - - - name: Upload Installers - uses: actions/upload-artifact@v2 - with: - name: LinuxInstallers - path: | - out/linux/Packaging.Linux/deb/Release/*.deb - out/linux/Packaging.Linux/tar/Release/*.tar.gz - - sign: - name: 'Sign' - runs-on: windows-latest - needs: build - steps: - - name: setup python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - uses: actions/checkout@v2 - - - name: 'Download Installer Artifact' - uses: actions/download-artifact@v2 - with: - name: LinuxInstallers - - - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: 'Install ESRP Client' - shell: pwsh - env: - AZ_SUB: ${{ secrets.AZURE_SUBSCRIPTION }} - run: | - az storage blob download --subscription "$env:AZ_SUB" --account-name gitcitoolstore -c tools -n microsoft.esrpclient.1.2.47.nupkg -f esrp.zip - Expand-Archive -Path esrp.zip -DestinationPath .\esrp - - - name: Install Certs - shell: pwsh - env: - AZ_SUB: ${{ secrets.AZURE_SUBSCRIPTION }} - AZ_VAULT: ${{ secrets.AZURE_VAULT }} - SSL_CERT: ${{ secrets.VAULT_SSL_CERT_NAME }} - ESRP_CERT: ${{ secrets.VAULT_ESRP_CERT_NAME }} - run: | - az keyvault secret download --subscription "$env:AZ_SUB" --vault-name "$env:AZ_VAULT" --name "$env:SSL_CERT" -f out.pfx - certutil -f -importpfx out.pfx - Remove-Item out.pfx - - az keyvault secret download --subscription "$env:AZ_SUB" --vault-name "$env:AZ_VAULT" --name "$env:ESRP_CERT" -f out.pfx - certutil -f -importpfx out.pfx - Remove-Item out.pfx - - - name: Run ESRP Client - shell: pwsh - env: - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - run: | - python .github/run_esrp_signing.py - - - name: Upload Installer - uses: actions/upload-artifact@v2 - with: - name: DebianInstallerSigned - path: | - Signed/*.deb \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bea685ec4..e4ead0dc0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,13 +23,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 # patch around Nerdbank.GitVersioning failure # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} @@ -37,4 +37,4 @@ jobs: dotnet build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 47c68a10a..7a06deff1 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -17,12 +17,12 @@ jobs: os: [ubuntu-18.04, ubuntu-20.04, windows-2019, macos-10.15] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: 6.0.201 @@ -42,4 +42,4 @@ jobs: run: dotnet build --configuration MacRelease - name: Test - run: dotnet test --no-restore --verbosity normal + run: dotnet test --verbosity normal diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml new file mode 100644 index 000000000..c723963ea --- /dev/null +++ b/.github/workflows/lint-docs.yml @@ -0,0 +1,19 @@ +name: "Lint documentation" + +on: + workflow_dispatch: + push: + branches: [ main, linux ] + pull_request: + branches: [ main, linux ] + +jobs: + lint-markdown: + name: Lint markdown files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: DavidAnson/markdownlint-cli2-action@744f913a124058ee903768d3adb92a4847e5d132 + with: + globs: "**/*.md" diff --git a/.github/workflows/release-homebrew.yaml b/.github/workflows/release-homebrew.yaml index b847388f9..ca657863b 100644 --- a/.github/workflows/release-homebrew.yaml +++ b/.github/workflows/release-homebrew.yaml @@ -14,5 +14,5 @@ jobs: tap: microsoft/git name: git-credential-manager-core type: cask - releaseAsset: gcmcore-osx-(.*)\.pkg + releaseAsset: gcm-osx-x64-(.*)\.pkg alwaysUsePullRequest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..db6dc6807 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,563 @@ +name: release + +on: + workflow_dispatch: + push: + branches: [ release ] + +jobs: +# ================================ +# macOS +# ================================ + osx-build: + name: Build macOS + runs-on: macos-latest + strategy: + matrix: + runtime: [ osx-x64, osx-arm64 ] + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. + + - name: Set up dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.201 + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: | + dotnet build src/osx/Installer.Mac/*.csproj \ + --configuration=MacRelease --no-self-contained \ + --runtime=${{ matrix.runtime }} + + - name: Run macOS unit tests + run: | + dotnet test --configuration=MacRelease + + - name: Lay out payload and symbols + run: | + src/osx/Installer.Mac/layout.sh \ + --configuration=MacRelease --output=payload \ + --symbol-output=symbols --runtime=${{ matrix.runtime }} + + - name: Create keychain + env: + CERT_BASE64: ${{ secrets.DEVELOPER_CERTIFICATE_BASE64 }} + CERT_PASSPHRASE: ${{ secrets.DEVELOPER_CERTIFICATE_PASSWORD }} + run: | + security create-keychain -p pwd $RUNNER_TEMP/buildagent.keychain + security default-keychain -s $RUNNER_TEMP/buildagent.keychain + security unlock-keychain -p pwd $RUNNER_TEMP/buildagent.keychain + echo $CERT_BASE64 | base64 -D > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/buildagent.keychain -P $CERT_PASSPHRASE -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $RUNNER_TEMP/buildagent.keychain + + - name: Developer sign + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + .github/run_developer_signing.sh payload $APPLE_TEAM_ID $GITHUB_WORKSPACE/src/osx/Installer.Mac/entitlements.xml + + - name: Upload macOS artifacts + uses: actions/upload-artifact@v3 + with: + name: tmp.${{ matrix.runtime }}-build + path: | + payload + symbols + + osx-payload-sign: + name: Sign macOS payload + # ESRP service requires signing to run on Windows + runs-on: windows-latest + strategy: + matrix: + runtime: [ osx-x64, osx-arm64 ] + needs: osx-build + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Download payload + uses: actions/download-artifact@v3 + with: + name: tmp.${{ matrix.runtime }}-build + + - name: Zip unsigned payload + shell: pwsh + run: | + Compress-Archive -Path payload payload/payload.zip + cd payload + Get-ChildItem -Exclude payload.zip | Remove-Item -Recurse -Force + + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set up ESRP client + shell: pwsh + env: + AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }} + AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} + REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} + run: | + .github\set_up_esrp.ps1 + + - name: Run ESRP client + shell: pwsh + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + # We temporarily need two AAD IDs, as we're using an SSL certificate associated + # with an older App Registration until we have the required hardware to approve + # the new certificate in SSL Admin. + AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }} + APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }} + APPLE_SIGNING_OP_CODE: ${{ secrets.APPLE_SIGNING_OPERATION_CODE }} + run: | + python .github\run_esrp_signing.py payload ` + $env:APPLE_KEY_CODE $env:APPLE_SIGNING_OP_CODE ` + --params 'Hardening' '--options=runtime' + + - name: Unzip signed payload + shell: pwsh + run: | + Expand-Archive signed/payload.zip -DestinationPath signed + Remove-Item signed/payload.zip + + - name: Upload signed payload + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.runtime }}-payload-sign + path: | + signed + + osx-pack: + name: Package macOS payload + runs-on: macos-latest + strategy: + matrix: + runtime: [ osx-x64, osx-arm64 ] + needs: osx-payload-sign + steps: + - name: Check out repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. + + - name: Set up dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.201 + + # Install Nerdbank.GitVersioning + - uses: dotnet/nbgv@master + with: + setCommonVars: true + + - name: Download signed payload + uses: actions/download-artifact@v3 + with: + name: ${{ matrix.runtime }}-payload-sign + + - name: Create component package + run: | + src/osx/Installer.Mac/pack.sh --payload=payload \ + --version=$GitBuildVersionSimple \ + --output=components/com.microsoft.gitcredentialmanager.component.pkg + + - name: Create product archive + run: | + src/osx/Installer.Mac/dist.sh --package-path=components \ + --version=$GitBuildVersionSimple --runtime=${{ matrix.runtime }} \ + --output=pkg/gcm-${{ matrix.runtime }}-$GitBuildVersionSimple.pkg || exit 1 + + - name: Upload package + uses: actions/upload-artifact@v3 + with: + name: tmp.${{ matrix.runtime }}-pack + path: | + pkg + + osx-sign: + name: Sign and notarize macOS package + # ESRP service requires signing to run on Windows + runs-on: windows-latest + strategy: + matrix: + runtime: [ osx-x64, osx-arm64 ] + needs: osx-pack + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Download unsigned package + uses: actions/download-artifact@v3 + with: + name: tmp.${{ matrix.runtime }}-pack + path: pkg + + - name: Zip unsigned package + shell: pwsh + run: | + Compress-Archive -Path pkg/*.pkg pkg/gcm-pkg.zip + cd pkg + Get-ChildItem -Exclude gcm-pkg.zip | Remove-Item -Recurse -Force + + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set up ESRP client + shell: pwsh + env: + AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }} + AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} + REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} + run: | + .github\set_up_esrp.ps1 + + - name: Sign package + shell: pwsh + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + # We temporarily need two AAD IDs, as we're using an SSL certificate associated + # with an older App Registration until we have the required hardware to approve + # the new certificate in SSL Admin. + AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }} + APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }} + APPLE_SIGNING_OP_CODE: ${{ secrets.APPLE_SIGNING_OPERATION_CODE }} + run: | + python .github\run_esrp_signing.py pkg $env:APPLE_KEY_CODE $env:APPLE_SIGNING_OP_CODE + + - name: Unzip signed package + shell: pwsh + run: | + mkdir unsigned + Expand-Archive -LiteralPath signed\gcm-pkg.zip -DestinationPath .\unsigned -Force + Remove-Item signed\gcm-pkg.zip -Force + + - name: Notarize signed package + shell: pwsh + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + # We temporarily need two AAD IDs, as we're using an SSL certificate associated + # with an older App Registration until we have the required hardware to approve + # the new certificate in SSL Admin. + AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }} + APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }} + APPLE_NOTARIZATION_OP_CODE: ${{ secrets.APPLE_NOTARIZATION_OPERATION_CODE }} + run: | + python .github\run_esrp_signing.py unsigned $env:APPLE_KEY_CODE $env:APPLE_NOTARIZATION_OP_CODE --params 'BundleId' 'com.microsoft.gitcredentialmanager' + + - name: Publish signed package + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.runtime }}-sign + path: signed/*.pkg + +# ================================ +# Windows +# ================================ + win-sign: + name: Build and Sign Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. + + - name: Set up dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.201 + + # Install Nerdbank.GitVersioning + - uses: dotnet/nbgv@master + with: + setCommonVars: true + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: | + dotnet build --configuration=WindowsRelease + + - name: Run Windows unit tests + run: | + dotnet test --configuration=WindowsRelease + + - name: Lay out Windows payload and symbols + shell: pwsh + run: | + cd src/windows/Installer.Windows/ + ./layout.ps1 -Configuration WindowsRelease -Output payload -SymbolOutput symbols + mkdir unsigned-payload + Get-ChildItem -Path payload/* -Include *.exe, *.dll | Move-Item -Destination unsigned-payload + + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set up ESRP client + shell: pwsh + env: + AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }} + AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} + REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} + run: | + .github\set_up_esrp.ps1 + + - name: Run ESRP client for unsigned payload + shell: pwsh + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + # We temporarily need two AAD IDs, as we're using an SSL certificate associated + # with an older App Registration until we have the required hardware to approve + # the new certificate in SSL Admin. + AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }} + WINDOWS_KEY_CODE: ${{ secrets.WINDOWS_KEY_CODE }} + WINDOWS_OP_CODE: ${{ secrets.WINDOWS_OPERATION_CODE }} + run: | + python .github\run_esrp_signing.py ` + src/windows/Installer.Windows/unsigned-payload ` + $env:WINDOWS_KEY_CODE $env:WINDOWS_OP_CODE ` + --params 'OpusName' 'Microsoft' ` + 'OpusInfo' 'http://www.microsoft.com' ` + 'FileDigest' '/fd "SHA256"' 'PageHash' '/NPH' ` + 'TimeStamp' '/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256' + + - name: Lay out signed payload + shell: pwsh + run: | + mkdir signed-payload + Move-Item -Path signed/* -Destination signed-payload + # ESRP will not sign the *.exe.config or NOTICE files, but they are needed to build the installers. + # Due to this, we copy them after signing. + Get-ChildItem -Path src/windows/Installer.Windows/payload/* -Include *.exe.config, NOTICE | Move-Item -Destination signed-payload + Remove-Item signed -Recurse -Force + + - name: Build with signed payload + shell: pwsh + run: | + dotnet build src/windows/Installer.Windows /p:PayloadPath=$env:GITHUB_WORKSPACE/signed-payload /p:NoLayout=true --configuration=WindowsRelease + + - name: Run ESRP client for installers + shell: pwsh + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + # We temporarily need two AAD IDs, as we're using an SSL certificate associated + # with an older App Registration until we have the required hardware to approve + # the new certificate in SSL Admin. + AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }} + WINDOWS_KEY_CODE: ${{ secrets.WINDOWS_KEY_CODE }} + WINDOWS_OP_CODE: ${{ secrets.WINDOWS_OPERATION_CODE }} + run: | + python .github\run_esrp_signing.py ` + .\out\windows\Installer.Windows\bin\WindowsRelease\net472 ` + $env:WINDOWS_KEY_CODE ` + $env:WINDOWS_OP_CODE ` + --params 'OpusName' 'Microsoft' ` + 'OpusInfo' 'http://www.microsoft.com' ` + 'FileDigest' '/fd "SHA256"' 'PageHash' '/NPH' ` + 'TimeStamp' '/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256' + + - name: Publish final artifacts + uses: actions/upload-artifact@v3 + with: + name: win-sign + path: | + signed + signed-payload + src/windows/Installer.Windows/symbols + +# ================================ +# Linux +# ================================ + linux-build: + name: Build Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. + + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.201 + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration=LinuxRelease + + - name: Lay out + run: | + mkdir -p linux-build/deb linux-build/tar + mv out/linux/Packaging.Linux/deb/Release/*.deb linux-build/deb + mv out/linux/Packaging.Linux/tar/Release/*.tar.gz linux-build/tar + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: linux-build + path: | + linux-build + + linux-sign: + name: Sign Debian package + # ESRP service requires signing to run on Windows + runs-on: windows-latest + needs: linux-build + steps: + - uses: actions/checkout@v3 + + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: linux-build + path: artifacts + + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set up ESRP client + shell: pwsh + env: + AZURE_STORAGE_KEY: ${{ secrets.AZURE_STORAGE_KEY }} + AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} + REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} + run: | + .github\set_up_esrp.ps1 + + - name: Run ESRP client + shell: pwsh + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + # We temporarily need two AAD IDs, as we're using an SSL certificate associated + # with an older App Registration until we have the required hardware to approve + # the new certificate in SSL Admin. + AZURE_AAD_ID_SSL: ${{ secrets.AZURE_AAD_ID_SSL }} + LINUX_KEY_CODE: ${{ secrets.LINUX_KEY_CODE }} + LINUX_OP_CODE: ${{ secrets.LINUX_OPERATION_CODE }} + run: | + python .github/run_esrp_signing.py artifacts/deb $env:LINUX_KEY_CODE $env:LINUX_OP_CODE + + - name: Upload signed Debian package + uses: actions/upload-artifact@v3 + with: + name: linux-sign + path: | + signed + +# ================================ +# Publish +# ================================ + create-github-release: + name: Publish GitHub draft release + runs-on: ubuntu-latest + needs: [ osx-sign, win-sign, linux-sign ] + steps: + - name: Check out repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. + + - name: Set up dotnet + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.201 + + # Install Nerdbank.GitVersioning + - uses: dotnet/nbgv@master + with: + setCommonVars: true + + - name: Download artifacts + uses: actions/download-artifact@v3 + + - name: Archive macOS payload and symbols + run: | + mkdir osx-payload-and-symbols + + tar -C osx-x64-payload-sign -czf osx-payload-and-symbols/gcm-osx-x64-$GitBuildVersionSimple.tar.gz . + tar -C tmp.osx-x64-build/symbols -czf osx-payload-and-symbols/gcm-osx-x64-$GitBuildVersionSimple-symbols.tar.gz . + + tar -C osx-arm64-payload-sign -czf osx-payload-and-symbols/gcm-osx-arm64-$GitBuildVersionSimple.tar.gz . + tar -C tmp.osx-arm64-build/symbols -czf osx-payload-and-symbols/gcm-osx-arm64-$GitBuildVersionSimple-symbols.tar.gz . + + - name: Archive Windows payload and symbols + shell: pwsh + run: | + mkdir win-x86-payload-and-symbols + Compress-Archive -Path win-sign/signed-payload/* win-x86-payload-and-symbols/gcm-win-x86-$env:GitBuildVersionSimple.zip + Compress-Archive -Path win-sign/src/windows/Installer.Windows/symbols/* win-x86-payload-and-symbols/gcm-win-x86-$env:GitBuildVersionSimple-symbols.zip + + - uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const version = process.env.GitBuildVersionSimple + + var releaseMetadata = { + owner: context.repo.owner, + repo: context.repo.repo + }; + + // Create the release + var tagName = `v${version}`; + var createdRelease = await github.rest.repos.createRelease({ + ...releaseMetadata, + draft: true, + tag_name: tagName, + name: `GCM ${version}` + }); + releaseMetadata.release_id = createdRelease.data.id; + + // Uploads contents of directory to the release created above + async function uploadDirectoryToRelease(directory, includeExtensions=[]) { + return fs.promises.readdir(directory) + .then(async(files) => Promise.all( + files.filter(file => { + return includeExtensions.length==0 || includeExtensions.includes(path.extname(file).toLowerCase()); + }) + .map(async (file) => { + var filePath = path.join(directory, file); + github.rest.repos.uploadReleaseAsset({ + ...releaseMetadata, + name: file, + headers: { + "content-length": (await fs.promises.stat(filePath)).size + }, + data: fs.createReadStream(filePath) + }); + })) + ); + } + + await Promise.all([ + // Upload Windows artifacts + uploadDirectoryToRelease('win-sign/signed'), + uploadDirectoryToRelease('win-x86-payload-and-symbols'), + + // Upload macOS artifacts + uploadDirectoryToRelease('osx-x64-sign'), + uploadDirectoryToRelease('osx-arm64-sign'), + uploadDirectoryToRelease('osx-payload-and-symbols'), + + // Upload Linux artifacts + uploadDirectoryToRelease('linux-sign'), + uploadDirectoryToRelease('linux-build/tar') + ]); diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml index 23e61aa89..f6648e602 100644 --- a/.github/workflows/validate-install-from-source.yml +++ b/.github/workflows/validate-install-from-source.yml @@ -22,7 +22,9 @@ jobs: - image: alpine container: ${{matrix.vector.image}} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. - run: | if [ ${{matrix.vector.image}} == "centos" ]; then sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-Linux-* diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 000000000..6e0ac4ada --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,6 @@ +// For information on writing markdownlint configuration see: +// https://github.com/DavidAnson/markdownlint/blob/main/README.md#optionsconfig +{ + "MD013": false, // Line length and line breaking convention not yet standardised across docs + "MD024": false // The format for some files require repeated headings, e.g. "Example" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 3a64696bc..a7a7e0e63 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -67,10 +67,12 @@ members of the project's leadership. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][cc-homepage], version 1.4, +available at [Contributor Covenant Code of Conduct][cc-coc]. -[homepage]: https://www.contributor-covenant.org +For answers to common questions about this code of conduct, see the +[Contributor Covenant FAQ][cc-faq] -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +[cc-homepage]: https://www.contributor-covenant.org +[cc-coc]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +[cc-faq]: https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff2f5b2ee..41c925a91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,43 +1,44 @@ -## Contributing +# Contributing -[issue]: https://github.com/GitCredentialManager/git-credential-manager/issues +[issue]: https://github.com/GitCredentialManager/git-credential-manager/issues/new/choose [fork]: https://github.com/GitCredentialManager/git-credential-manager/fork [pr]: https://github.com/GitCredentialManager/git-credential-manager/compare [code-of-conduct]: CODE_OF_CONDUCT.md +[commits]: https://www.youtube.com/watch?v=4qLtKx9S9a8 -Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. +Hi there! We're thrilled that you'd like to contribute to GCM :tada:. Your help is essential for keeping it great. -Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE). +Contributions to GCM are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE). Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. ## Start with an issue -0. Open an [issue][issue] to discuss the change you want to see. +1. Open an [issue][issue] to discuss the change you want to see. This helps us coordinate and reduce duplication. -0. Once we've had some discussion, you're ready to code! +1. Once we've had some discussion, you're ready to code! ## Submitting a pull request -0. [Fork][fork] and clone the repository -0. Configure and install the dependencies: `dotnet restore` -0. Make sure the tests pass on your machine: `dotnet test` -0. Create a new branch: `git switch -c my-branch-name` -0. Make your change, add tests, and make sure the tests still pass -0. For UI updates, test your changes by executing a `dotnet run` in applicable UI-related project directories: +1. [Fork][fork] and clone the repository +1. Configure and install the dependencies: `dotnet restore` +1. Make sure the tests pass on your machine: `dotnet test` +1. Create a new branch: `git switch -c my-branch-name` +1. Make your change, add tests, and make sure the tests still pass +1. For UI updates, test your changes by executing a `dotnet run` in applicable UI-related project directories: - `Atlassian.Bitbucket.UI.Avalonia` - `GitHub.UI.Avalonia` - `Atlassian.Bitbucket.UI.Windows` - `GitHub.UI.Windows` -0. Push to your fork and [submit a pull request][pr] -0. Pat your self on the back and wait for your pull request to be reviewed and merged. +1. Organize your changes into one or more [logical, descriptive commits][commits]. +1. Push to your fork and [submit a pull request][pr] +1. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Match existing code style. - Write tests. - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. -- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). ## Resources diff --git a/Directory.Build.props b/Directory.Build.props index 7f78555c7..3abe378a3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,12 @@ $(RepoPath)src\ $(RepoPath)out\ $(RepoPath)assets\ + + + <_IsExeProject Condition="'$(OutputType)' == 'Exe' OR '$(OutputType)' == 'WinExe'">true + + + true diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..3c84f230d --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,27 @@ + + + + + + + + + + $(IntermediateOutputPath)app.manifest + + + + + + + + + + + diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln index 0330e693c..9b0e76ba8 100644 --- a/Git-Credential-Manager.sln +++ b/Git-Credential-Manager.sln @@ -31,8 +31,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer.Mac", "src\osx\In EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer.Windows", "src\windows\Installer.Windows\Installer.Windows.csproj", "{85903170-9E52-4B53-A6E4-3F416F684FAE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Payload.Windows", "src\windows\Payload.Windows\Payload.Windows.csproj", "{8DBBAB0A-970D-4BE3-958C-8CDC92F76549}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket", "src\shared\Atlassian.Bitbucket\Atlassian.Bitbucket.csproj", "{B49881A6-E734-490E-8EA7-FB0D9E296CFB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.Tests", "src\shared\Atlassian.Bitbucket.Tests\Atlassian.Bitbucket.Tests.csproj", "{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}" @@ -229,16 +227,6 @@ Global {85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU {85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU {85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -499,7 +487,6 @@ Global {3D279E2D-E011-45CF-8EA8-3D71D1300443} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} {74FA0AA4-B5C1-4F3B-B182-277FC2D50715} = {3D279E2D-E011-45CF-8EA8-3D71D1300443} {85903170-9E52-4B53-A6E4-3F416F684FAE} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} - {8DBBAB0A-970D-4BE3-958C-8CDC92F76549} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} {B49881A6-E734-490E-8EA7-FB0D9E296CFB} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} {025E5329-A0B1-4BA9-9203-B70B44A5F9E0} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} {2B3CD8FF-84A6-4B53-A28B-D7A75B0AB4D7} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} diff --git a/README.md b/README.md index 0536d8364..e7e4c7472 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ [Git Credential Manager](https://github.com/GitCredentialManager/git-credential-manager) (GCM) is a secure Git credential helper built on [.NET](https://dotnet.microsoft.com) that runs on Windows, macOS, and Linux. -Compared to Git's [built-in credential helpers]((https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage)) (Windows: wincred, macOS: osxkeychain, Linux: gnome-keyring/libsecret) which provides single-factor authentication support working on any HTTP-enabled Git repository, GCM provides multi-factor authentication support for [Azure DevOps](https://dev.azure.com/), Azure DevOps Server (formerly Team Foundation Server), GitHub, and Bitbucket. +Compared to Git's [built-in credential helpers]((https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage)) (Windows: wincred, macOS: osxkeychain, Linux: gnome-keyring/libsecret) which provides single-factor authentication support working on any HTTP-enabled Git repository, GCM provides multi-factor authentication support for [Azure DevOps](https://dev.azure.com/), Azure DevOps Server (formerly Team Foundation Server), GitHub, Bitbucket, and GitLab. Git Credential Manager (GCM) replaces the .NET Framework-based [Git Credential Manager for Windows](https://github.com/microsoft/Git-Credential-Manager-for-Windows) (GCM), and the Java-based [Git Credential Manager for Mac and Linux](https://github.com/microsoft/Git-Credential-Manager-for-Mac-and-Linux) (Java GCM), providing a consistent authentication experience across all platforms. ## Current status -Git Credential Manager is currently available for Windows, macOS, and Linux. GCM only works with HTTP(S) remotes; you can still use Git with SSH: +Git Credential Manager is currently available for Windows, macOS, and Linux\*. GCM only works with HTTP(S) remotes; you can still use Git with SSH: - [Azure DevOps SSH](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops) - [GitHub SSH](https://help.github.com/en/articles/connecting-to-github-with-ssh) @@ -21,7 +21,7 @@ Git Credential Manager is currently available for Windows, macOS, and Linux. GCM Feature|Windows|macOS|Linux -|:-:|:-:|:-: Installer/uninstaller|✓|✓|✓\* -Secure platform credential storage|✓
[(see more)](docs/credstores.md)|✓
[(see more)](docs/credstores.md)|✓
[(see more)](docs/credstores.md) +Secure platform credential storage|✓ [(see more)](docs/credstores.md)|✓ [(see more)](docs/credstores.md)|✓ [(see more)](docs/credstores.md) Multi-factor authentication support for Azure DevOps|✓|✓|✓ Two-factor authentication support for GitHub|✓|✓|✓ Two-factor authentication support for Bitbucket|✓|✓|✓ @@ -34,9 +34,11 @@ Proxy support|✓|✓|✓ `arm64` support|best effort|via Rosetta 2|best effort, no packages `armhf` support|_N/A_|_N/A_|best effort, no packages -**Notes:** +(\*) GCM guarantees support for the below Linux distributions. GCM maintainers also monitor and evaluate issues opened against other distributions to determine community interest/engagement and whether an emerging platform should become fully-supported. -(\*) Fedora packages planned but not yet available. +- Debian/Ubuntu/Linux Mint +- Fedora/CentOS/RHEL +- Alpine ## Download and Install @@ -86,34 +88,10 @@ sudo /usr/local/share/gcm-core/uninstall.sh --- + -### Linux - -#### Experimental: install from source helper script - -If you would like to help dogfood our new install from source helper script, -run the following: - -1. To ensure `curl` is installed: - -```shell -curl --version -``` - -If `curl` is not installed, please use your distribution's package manager -to install it. - -0. To 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. +### Linux #### Ubuntu/Debian distributions @@ -123,7 +101,8 @@ Download the latest [.deb package](https://github.com/GitCredentialManager/git-c sudo dpkg -i git-credential-manager-core configure ``` -__Note:__ Although packages were previously offered on certain + +**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. @@ -137,6 +116,8 @@ 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 @@ -151,6 +132,33 @@ 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. --- @@ -171,11 +179,11 @@ Installing GCM as a standalone package on Windows will forcibly override the ver There are two flavors of standalone installation on Windows: -- User (preferred) (`gcmcoreuser-win*`): +- 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 (`gcmcore-win*`): +- System (`gcm-win*`): Requires administrator rights. Will install for all users on the system and update the system-wide Git configuration. @@ -239,15 +247,16 @@ See detailed information [here](https://aka.ms/gcm/httpproxy). - [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) ## Experimental Features - [Windows broker (experimental)](docs/windows-broker.md) -- [Azure Repos OAuth tokens (experimental)](docs/azrepos-users-and-tokens.md) ## Contributing -This project welcomes contributions and suggestions. +This project welcomes contributions and suggestions. See the [contributing guide](CONTRIBUTING.md) to get started. This project follows [GitHub's Open Source Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/SECURITY.md b/SECURITY.md index 5b2208269..8785fd5ba 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,3 +1,5 @@ +# Security + If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github) Thanks for helping make GitHub products safe for everyone. diff --git a/build/GCM.MSBuild.csproj b/build/GCM.MSBuild.csproj new file mode 100644 index 000000000..7dbe90afe --- /dev/null +++ b/build/GCM.MSBuild.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + false + + + + + + + + diff --git a/build/GCM.tasks b/build/GCM.tasks new file mode 100644 index 000000000..fe7031f34 --- /dev/null +++ b/build/GCM.tasks @@ -0,0 +1,16 @@ + + + <_TaskAssembly>$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll + <_TaskFactory>CodeTaskFactory + + + <_TaskAssembly>$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll + <_TaskFactory>RoslynCodeTaskFactory + + + + + + + + diff --git a/build/GenerateWindowsAppManifest.cs b/build/GenerateWindowsAppManifest.cs new file mode 100644 index 000000000..58a94c5a1 --- /dev/null +++ b/build/GenerateWindowsAppManifest.cs @@ -0,0 +1,51 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.IO; + +namespace GitCredentialManager.MSBuild +{ + public class GenerateWindowsAppManifest : Task + { + [Required] + public string Version { get; set; } + + [Required] + public string ApplicationName { get; set; } + + [Required] + public string OutputFile { get; set; } + + public override bool Execute() + { + Log.LogMessage(MessageImportance.Normal, "Creating application manifest file for '{0}'...", ApplicationName); + + string manifestDirectory = Path.GetDirectoryName(OutputFile); + if (!Directory.Exists(manifestDirectory)) + { + Directory.CreateDirectory(manifestDirectory); + } + + File.WriteAllText( + OutputFile, + $@" + + + + + + + + + + + + + + + +"); + + return true; + } + } +} diff --git a/docs/architecture.md b/docs/architecture.md index eee92e3e3..7790dfc8c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -31,13 +31,13 @@ | | | | | | +-v---v----v--------------v------------+ +-v-----------------v----------------+ | | | | -| Core <--+ Core.UI | +| Core <--+ Core.UI | | | | | +--------------------------------------+ +------------------------------------+ ``` Git Credential Manager (GCM) is built to be Git host and platform/OS -agonstic. Most of the shared logic (command execution, the abstract platform +agnostic. Most of the shared logic (command execution, the abstract platform subsystems, etc) can be found in the `Core` class library (C#). The library targets .NET Standard as well as .NET Framework. @@ -74,7 +74,7 @@ the shared, core binaries shell out to. Currently the Bitbucket and GitHub providers each have a WPF (Windows only) helper executable that shows authentication prompts and messages. -The `Microsoft.Git.CredentialHelper.UI` project is a WPF (Windows only) assembly +The `Core.UI` project is a WPF (Windows only) assembly that contains common WPF components and styles that are shared between provider helpers on Windows. @@ -163,7 +163,7 @@ 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) -configuration and environment variable respectively (3)). +configuration and environment variable respectively (3). The `Get|Store|EraseCommand`s call the corresponding `Get|Store|EraseCredentialAsync` methods on the `IHostProvider`, passing the diff --git a/docs/azrepos-users-and-tokens.md b/docs/azrepos-users-and-tokens.md index 153e7f8d4..1aab32262 100644 --- a/docs/azrepos-users-and-tokens.md +++ b/docs/azrepos-users-and-tokens.md @@ -5,11 +5,11 @@ The Azure Repos host provider supports creating multiple types of credential: - Azure DevOps personal access tokens -- Microsoft identity OAuth tokens (experimental) +- 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-experimental) -configuration entry (or [`GCM_AZREPOS_CREDENTIALTYPE`](environment.md#GCM_AZREPOS_CREDENTIALTYPE-experimental) +and use, you can set the [`credential.azreposCredentialType`](configuration.md#credentialazreposcredentialtype) +configuration entry (or [`GCM_AZREPOS_CREDENTIALTYPE`](environment.md#GCM_AZREPOS_CREDENTIALTYPE) environment variable). ### Azure DevOps personal access tokens @@ -24,7 +24,7 @@ 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 resulted in a new authentication prompt. -### Microsoft identity OAuth tokens (experimental) +### Microsoft identity OAuth tokens "Microsoft identity OAuth token" is the generic term for OAuth-based access tokens issued by Azure Active Directory for either Work and School Accounts @@ -64,7 +64,7 @@ credential. This may change in the future. Normally you won't need to worry about managing which user accounts Git Credential Manager is using as this is configured automatically when you first -authenticate for a particular Azure DevOps organziation. +authenticate for a particular Azure DevOps organization. In advanced scenarios (such as using multiple accounts) you can interact with and manage remembered user accounts using the 'azure-repos' provider command: @@ -181,7 +181,7 @@ fabrikam: ``` In the above example, the `~/myrepo` repository has a single Git remote named -`origin` that points to the `contoso` Azure DevOps organziation. There is no +`origin` that points to the `contoso` Azure DevOps organization. There is no user account specifically associated with the `origin` remote, so the global user account binding for `contoso` will be used (the global binding is inherited). diff --git a/docs/bitbucket-development.md b/docs/bitbucket-development.md index 26dddda90..dc7140770 100644 --- a/docs/bitbucket-development.md +++ b/docs/bitbucket-development.md @@ -63,20 +63,20 @@ Assuming the user successfully logins into Bitbucket and authorizes the GCM this 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 -On-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC, has a number of differences compared to the cloud instance of Bitbucket, 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](https://bitbucket.org). 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: - ❯ atlas-run-standalone --product bitbucket + ❯ atlas-run-standalone --product bitbucket -See https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/. +See the developer documentation for [atlas-run-standalone](https://developer.atlassian.com/server/framework/atlassian-sdk/atlas-run-standalone/). 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 + https://localhost:7990/bitbucket -Instructions on how to download and install the Atlassian SDK can be found here: https://developer.atlassian.com/server/framework/atlassian-sdk/ +Atlassian has [documentation](https://developer.atlassian.com/server/framework/atlassian-sdk/) on how to download and install their SDK. diff --git a/docs/configuration.md b/docs/configuration.md index c1c14761a..e295c68d7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,8 +39,8 @@ The following table summarizes the change in behavior and the mapping of older v Value(s)|Old meaning|New meaning -|-|- `auto`|Prompt if required – use cached credentials if possible|_(unchanged)_ -`never`,
`false`| Never prompt – fail if interaction is required|_(unchanged)_ -`always`,
`force`,
`true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value) +`never`, `false`| Never prompt – fail if interaction is required|_(unchanged)_ +`always`, `force`, `true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value) #### Example @@ -64,7 +64,7 @@ ID|Provider `azure-repos`|Azure Repos `github`|GitHub `bitbucket`|Bitbucket -`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_ +`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_ `generic`|Generic (any other provider not listed above) Automatic provider selection is based on the remote URL. @@ -92,11 +92,11 @@ Select the host provider to use when authenticating by which authority is suppor Authority|Provider(s) -|- `auto` _(default)_|_\[automatic\]_ -`msa`, `microsoft`, `microsoftaccount`,
`aad`, `azure`, `azuredirectory`,
`live`, `liveconnect`, `liveid`|Azure Repos
_(supports Microsoft Authentication)_ -`github`|GitHub
_(supports GitHub Authentication)_ -`bitbucket`|Bitbucket.org
_(supports Basic Authentication and OAuth)_
Bitbucket Server
_(supports Basic Authentication)_ -`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_ -`basic`, `integrated`, `windows`, `kerberos`, `ntlm`,
`tfs`, `sso`|Generic
_(supports Basic and Windows Integrated Authentication)_ +`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|Azure Repos _(supports Microsoft Authentication)_ +`github`|GitHub _(supports GitHub Authentication)_ +`bitbucket`|Bitbucket.org _(supports Basic Authentication and OAuth)_, Bitbucket Server _(supports Basic Authentication)_ +`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_ +`basic`, `integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|Generic _(supports Basic and Windows Integrated Authentication)_ #### Example @@ -196,7 +196,6 @@ git config --global credential.httpsProxy http://john.doe:password@proxy.contoso 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 supports multiple values separated by commas. @@ -227,7 +226,6 @@ Enabling this option will improve performance when using Oauth2 and interacting Enabling this option will decrease performance when using Basic Auth by requiring the user the re-enter credentials everytime. - Value|Refresh Credentials Before Returning -|- `true`, `1`, `yes`, `on` |Always @@ -295,7 +293,6 @@ git config --global credential.gitLabAuthModes "browser" --- - ### credential.namespace Use a custom namespace prefix for credentials read and written in the OS credential store. @@ -323,7 +320,7 @@ Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset o Value|Credential Store|Platforms -|-|- -_(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|- +_(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 `keychain`|macOS Keychain.|macOS @@ -332,7 +329,7 @@ _(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|- `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 -##### Example +#### Example ```bash git config --global credential.credentialStore gpg @@ -475,6 +472,7 @@ Credential: "git:https://github.com" (user = alice) https://github.com/contoso/widgets https://alice@github.com/contoso/widgets ``` + ```text Credential: "git:https://bob@github.com" (user = bob) @@ -489,17 +487,20 @@ Credential: "git:https://github.com/foo/bar" (user = alice) https://github.com/foo/bar ``` + ```text Credential: "git:https://github.com/contoso/widgets" (user = alice) https://github.com/contoso/widgets https://alice@github.com/contoso/widgets ``` + ```text Credential: "git:https://bob@github.com/foo/bar" (user = bob) https://bob@github.com/foo/bar ``` + ```text Credential: "git:https://bob@github.com/example/myrepo" (user = bob) @@ -508,7 +509,7 @@ Credential: "git:https://bob@github.com/example/myrepo" (user = bob) --- -### credential.azreposCredentialType _(experimental)_ +### credential.azreposCredentialType Specify the type of credential the Azure Repos host provider should return. @@ -519,7 +520,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-azuretokens.md). +More information about Azure Access tokens can be found [here](azrepos-users-and-tokens.md). #### Example @@ -527,4 +528,4 @@ More information about Azure Access tokens can be found [here](azrepos-azuretoke git config --global credential.azreposCredentialType oauth ``` -**Also see: [GCM_AZREPOS_CREDENTIALTYPE](environment.md#GCM_AZREPOS_CREDENTIALTYPE-experimental)** +**Also see: [GCM_AZREPOS_CREDENTIALTYPE](environment.md#GCM_AZREPOS_CREDENTIALTYPE)** diff --git a/docs/credstores.md b/docs/credstores.md index 3e4a232f2..bb206e647 100644 --- a/docs/credstores.md +++ b/docs/credstores.md @@ -178,7 +178,7 @@ TTY device path, as returned by the `tty` utility. ## Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache) -**Available on:** _Windows, macOS, Linux_ +**Available on:** _macOS, Linux_ ```shell export GCM_CREDENTIAL_STORE=cache diff --git a/docs/development.md b/docs/development.md index 0e8f2dc41..fdec0d8b7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -101,34 +101,35 @@ $ GCM_TRACE=1 git-credential-manager-core version 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 +dotnet test --collect:"XPlat Code Coverage" --settings=./.code-coverage/coverlet.settings.xml ``` Or via the VSCode Terminal/Run Task: -``` +```console test with coverage ``` 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 +dotnet ~/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage ``` + or ```shell -$ dotnet {$env:USERPROFILE}/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage +dotnet {$env:USERPROFILE}/.nuget/packages/reportgenerator/*/*/net6.0/ReportGenerator.dll -reports:./**/TestResults/**/coverage.cobertura.xml -targetdir:./out/code-coverage ``` Or via VSCode Terminal/Run Task: -``` +```console report coverage - nix ``` or -``` +```console report coverage - win -``` \ No newline at end of file +``` diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md index 754d0766d..96ce891d5 100644 --- a/docs/enterprise-config.md +++ b/docs/enterprise-config.md @@ -4,12 +4,12 @@ Git Credential Manager (GCM) can be configured using multiple different mechanisms. In order of preference, those mechanisms are: 1. [Environment variables](environment.md) -2. [Standard Git configuration files](configuration.md) +1. [Standard Git configuration files](configuration.md) 1. Repository/local configuration (`.git/config`) - 2. User/global configuration (`$HOME/.gitconfig` or `%HOME%\.gitconfig`) - 3. Installation/system configuration (`etc/gitconfig`) -3. Enterprise system administrator defaults -4. Compiled default values + 1. User/global configuration (`$HOME/.gitconfig` or `%HOME%\.gitconfig`) + 1. Installation/system configuration (`etc/gitconfig`) +1. Enterprise system administrator defaults +1. Compiled default values This model largely matches what Git itself supports, namely environment variables that take precedence over Git configuration files. @@ -18,25 +18,25 @@ The addition of the enterprise system administrator defaults enables those administrators to configure many GCM settings using familiar MDM tooling, rather than having to modify the Git installation configuration files. -### User Freedom +## User Freedom We believe the user should _always_ be at liberty to configure -Git and GCM exactly as they wish. By prefering environment variables and Git +Git and GCM exactly as they wish. By preferring environment variables and Git configuration files over system admin values, these only act as _default values_ -that can always be overriden by the user in the usual ways. +that can always be overridden by the user in the usual ways. ## Windows Default setting values come from the Windows Registry, specifically the following keys: -**32-bit Windows** +### 32-bit Windows ```text HKEY_LOCAL_MACHINE\SOFTWARE\GitCredentialManager\Configuration ``` -**64-bit Windows** +### 64-bit Windows ```text HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GitCredentialManager\Configuration @@ -55,7 +55,6 @@ those of the [Git configuration](configuration.md) settings. The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD` (integer). - ## macOS/Linux Default configuration setting stores has not been implemented. diff --git a/docs/environment.md b/docs/environment.md index bf50971a3..18cfa4774 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -137,8 +137,8 @@ The following table summarizes the change in behavior and the mapping of older v Value(s)|Old meaning|New meaning -|-|- `auto`|Prompt if required – use cached credentials if possible|_(unchanged)_ -`never`,
`false`| Never prompt – fail if interaction is required|_(unchanged)_ -`always`,
`force`,
`true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value) +`never`, `false`| Never prompt – fail if interaction is required|_(unchanged)_ +`always`, `force`, `true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value) #### Example @@ -169,7 +169,7 @@ ID|Provider `auto` _(default)_|_\[automatic\]_ ([learn more](autodetect.md)) `azure-repos`|Azure Repos `github`|GitHub -`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_ +`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_ `generic`|Generic (any other provider not listed above) Automatic provider selection is based on the remote URL. @@ -205,10 +205,10 @@ Select the host provider to use when authenticating by which authority is suppor Authority|Provider(s) -|- `auto` _(default)_|_\[automatic\]_ -`msa`, `microsoft`, `microsoftaccount`,
`aad`, `azure`, `azuredirectory`,
`live`, `liveconnect`, `liveid`|Azure Repos
_(supports Microsoft Authentication)_ -`github`|GitHub
_(supports GitHub Authentication)_ -`gitlab`|GitLab
_(supports OAuth in browser, personal access token and Basic Authentication)_ -`basic`, `integrated`, `windows`, `kerberos`, `ntlm`,
`tfs`, `sso`|Generic
_(supports Basic and Windows Integrated Authentication)_ +`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|Azure Repos _(supports Microsoft Authentication)_ +`github`|GitHub _(supports GitHub Authentication)_ +`gitlab`|GitLab _(supports OAuth in browser, personal access token and Basic Authentication)_ +`basic`, `integrated`, `windows`, `kerberos`, `ntlm`, `tfs`, `sso`|Generic _(supports Basic and Windows Integrated Authentication)_ #### Example @@ -325,13 +325,13 @@ Configure GCM to use the a proxy for network operations. **Note:** Git itself does _not_ respect this setting; this affects GCM _only_. -##### Windows +#### Windows ```batch SET GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_HTTP_PROXY=http://john.doe:password@proxy.contoso.com @@ -356,13 +356,13 @@ _(unset)_|Automatically detect modes `oauth`|OAuth-based authentication `basic`|Basic/PAT-based authentication -##### Windows +#### Windows ```batch SET GCM_BITBUCKET_AUTHMODES="oauth,basic" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_BITBUCKET_AUTHMODES="oauth,basic" @@ -380,21 +380,20 @@ This is especially relevant to OAuth credentials. Bitbucket.org access tokens ex 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 -|- `true`, `1`, `yes`, `on` |Always `false`, `0`, `no`, `off`_(default)_|Only when the credentials are found to be invalid -##### Windows +#### Windows ```batch SET GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS=1 ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS=1 @@ -422,13 +421,13 @@ _(unset)_|Automatically detect modes `basic`|Basic authentication using username and password `pat`|Personal Access Token (pat)-based authentication -##### Windows +#### Windows ```batch SET GCM_GITHUB_AUTHMODES="oauth,basic" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_GITHUB_AUTHMODES="oauth,basic" @@ -452,13 +451,13 @@ _(unset)_|Automatically detect modes `basic`|Basic authentication using username and password `pat`|Personal Access Token (pat)-based authentication -##### Windows +#### Windows ```batch SET GCM_GITLAB_AUTHMODES="browser" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_GITLAB_AUTHMODES="browser" @@ -475,13 +474,13 @@ Credentials will be stored in the format `{namespace}:{service}`. Defaults to the value `git`. -##### Windows +#### Windows ```batch SET GCM_NAMESPACE="my-namespace" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_NAMESPACE="my-namespace" @@ -501,7 +500,7 @@ Default value on Windows is `wincredman`, on macOS is `keychain`, and is unset o Value|Credential Store|Platforms -|-|- -_(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|- +_(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 `keychain`|macOS Keychain.|macOS @@ -510,13 +509,13 @@ _(unset)_|Windows: `wincredman`
macOS: `keychain`
Linux: _(none)_|- `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 -##### Windows +#### Windows ```batch SET GCM_CREDENTIAL_STORE="gpg" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_CREDENTIAL_STORE="gpg" @@ -597,7 +596,7 @@ Specify the path (_including_ the executable name) to the version of `gpg` used 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 +#### macOS/Linux ```bash export GCM_GPG_PATH="/usr/local/bin/gpg2" @@ -625,13 +624,13 @@ Value|Authentication Flow `system`|Open the user's default web browser. `devicecode`|Show a device code. -##### Windows +#### Windows ```batch SET GCM_MSAUTH_FLOW="devicecode" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_MSAUTH_FLOW="devicecode" @@ -654,13 +653,13 @@ Value|Description `true`|Use the operating system account manager as an authentication broker. `false` _(default)_|Do not use the broker. -##### Windows +#### Windows ```batch SET GCM_MSAUTH_USEBROKER="true" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_MSAUTH_USEBROKER="false" @@ -670,7 +669,7 @@ export GCM_MSAUTH_USEBROKER="false" --- -### GCM_AZREPOS_CREDENTIALTYPE _(experimental)_ +### GCM_AZREPOS_CREDENTIALTYPE Specify the type of credential the Azure Repos host provider should return. @@ -681,18 +680,18 @@ 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-azuretokens.md). +More information about Azure Access tokens can be found [here](azrepos-users-and-tokens.md). -##### Windows +#### Windows ```batch SET GCM_AZREPOS_CREDENTIALTYPE="oauth" ``` -##### macOS/Linux +#### macOS/Linux ```bash export GCM_AZREPOS_CREDENTIALTYPE="oauth" ``` -**Also see: [credential.azreposCredentialType](configuration.md#azreposcredentialtype-experimental)** +**Also see: [credential.azreposCredentialType](configuration.md#azreposcredentialtype)** diff --git a/docs/faq.md b/docs/faq.md index d95365cf9..b61d43d0e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -14,17 +14,17 @@ Please follow these steps to diagnose or resolve the problem: 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. -### Q: I got an error saying unsecure HTTP is not supported. +### 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). Please make sure your remote URLs use "https://" rather than "http://". -### Q: I got an authentication error and I am behind a network proxy. +### 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). -### Q: I'm getting errors about picking a credential store on Linux. +### 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. @@ -65,7 +65,7 @@ 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'? @@ -136,3 +136,31 @@ information. You may also set these variables to the empty string `""` to force terminal/ 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) +and pick "Revoke access". + +![Revoke GCM OAuth app access](img/github-oauthapp-revoke.png) + +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). + +### 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: + +1. Navigating to [the **Applications** page within your **User Settings**](https://gitlab.com/-/profile/applications). +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). + + ![Button to revoke GitLab OAuth Application access](./img/gitlab-oauthapp-revoke.png) +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). diff --git a/docs/github-apideprecation.md b/docs/github-apideprecation.md index a9c010714..9d5c3b9a3 100644 --- a/docs/github-apideprecation.md +++ b/docs/github-apideprecation.md @@ -53,42 +53,42 @@ GCM for Windows bundled with the Git for Windows installation. 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)! -## Help! I cannot make any changes to my Windows machine without an Administrator! +## 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` executable. -### Help! I still cannot or don't want to install anything! +### Help! I still cannot or don't want to install anything 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! 😁 -2. [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](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)) -3. Enter a name ("note") for the token and ensure the `repo`, `gist`, and +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](https://user-images.githubusercontent.com/5658207/95447304-6f5c6e80-0959-11eb-924b-50b86c2b3d77.png) -... -![image](https://user-images.githubusercontent.com/5658207/95447450-a3d02a80-0959-11eb-82a8-2d2834d5aa16.png) -... -![image](https://user-images.githubusercontent.com/5658207/95447343-7b483080-0959-11eb-8e00-151d53893f3f.png) + ![image](https://user-images.githubusercontent.com/5658207/95448332-1beb2000-095b-11eb-9a48-9c05b1926a6b.png) + ... + ![image](https://user-images.githubusercontent.com/5658207/95447304-6f5c6e80-0959-11eb-924b-50b86c2b3d77.png) + ... + ![image](https://user-images.githubusercontent.com/5658207/95447450-a3d02a80-0959-11eb-82a8-2d2834d5aa16.png) + ... + ![image](https://user-images.githubusercontent.com/5658207/95447343-7b483080-0959-11eb-8e00-151d53893f3f.png) -3. Click "Generate Token" +1. Click "Generate Token" -![image](https://user-images.githubusercontent.com/5658207/95448393-31f8e080-095b-11eb-9568-cfd1c567a65c.png) + ![image](https://user-images.githubusercontent.com/5658207/95448393-31f8e080-095b-11eb-9568-cfd1c567a65c.png) -4. **[IMPORTANT]** Keep the resulting page open as this contains your new token +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](https://user-images.githubusercontent.com/5658207/95448288-ff4ee800-095a-11eb-9709-8e37bde8b716.png) -5. Save the generated PAT in the Windows Credential Manager: +1. Save the generated PAT in the Windows Credential Manager: 1. If you prefer to use the command-line, open a command prompt (cmd.exe) and type the following: @@ -98,7 +98,7 @@ 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 Enter + 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) diff --git a/docs/gitlab.md b/docs/gitlab.md index c7eddbc06..94065398e 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -6,16 +6,16 @@ Git Credential Manager supports [gitlab.com](https://gitlab.com) out the box. 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 scope to 'write_repository'. -2. Copy the application ID and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientId ` -3. Copy the application secret and configure `git config --global credential.https://gitlab.example.com.GitLabDevClientSecret ` -4. Configure authentication modes to include 'browser' `git config --global credential.https://gitlab.example.com.gitLabAuthModes browser` -5. For good measure, configure `git config --global credential.https://gitlab.example.com.provider gitlab` -6. Verify the config is as expected `git config --global --get-urlmatch credential https://gitlab.example.com` +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` ### 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.provider @@ -23,22 +23,24 @@ To use on another instance, eg. `https://gitlab.example.com` requires setup and ## Preferences -``` +```console Select an authentication method for 'https://gitlab.com/': 1. Web browser (default) 2. Personal access token 3. Username/password -option (enter for default): +option (enter for default): ``` If you have a preferred authentication mode, you can specify [credential.gitLabAuthModes](configuration.md#credential.gitLabAuthModes): - `git config --global credential.gitlabauthmodes browser` +```console +git config --global credential.gitlabauthmodes browser +``` ## Caveats 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) https://gitlab.com/gitlab-org/gitlab/-/issues/332682 -2. Only domains with prefix `gitlab.` are recognised as GitLab remotes https://gitlab.com/gitlab-org/gitlab/-/issues/349464 -3. Username/password authentication is suggested even if disabled on server https://gitlab.com/gitlab-org/gitlab/-/issues/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) diff --git a/docs/img/github-oauthapp-revoke.png b/docs/img/github-oauthapp-revoke.png new file mode 100644 index 000000000..38151bdcf Binary files /dev/null and b/docs/img/github-oauthapp-revoke.png differ diff --git a/docs/img/gitlab-oauthapp-revoke.png b/docs/img/gitlab-oauthapp-revoke.png new file mode 100644 index 000000000..261fff96a Binary files /dev/null and b/docs/img/gitlab-oauthapp-revoke.png differ diff --git a/docs/img/gitlab-oauthapp-revoked.png b/docs/img/gitlab-oauthapp-revoked.png new file mode 100644 index 000000000..f478c152e Binary files /dev/null and b/docs/img/gitlab-oauthapp-revoked.png differ diff --git a/docs/linux-fromsrc-uninstall.md b/docs/linux-fromsrc-uninstall.md new file mode 100644 index 000000000..878528804 --- /dev/null +++ b/docs/linux-fromsrc-uninstall.md @@ -0,0 +1,55 @@ +# 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. + +: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. + +*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. + +```console +git-credential-manager-core unconfigure && +sudo rm $(command -v git-credential-manager-core) && +sudo rm -rf /usr/local/share/gcm-core && +sudo rm -rf ~/git-credential-manager && +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. + +```console +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). + +```console +sudo rm -rf ~/.dotnet && +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). + +```console +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). + +```console +sudo rm -rf ~/.dotnet && +sudo apk del icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib which bash coreutils gcompat git curl +``` diff --git a/docs/migration.md b/docs/migration.md index a2be4cfe7..88ba8da89 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -10,12 +10,12 @@ Because both Basic HTTP authentication and Windows Integrated Authentication (WI 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) +GCM_AUTHORITY (credential.authority)|→|GCM_PROVIDER (credential.provider)|GCM_ALLOW_WINDOWSAUTH (credential.allowWindowsAuth) -|-|-|- -`msa`, `microsoft`, `microsoftaccount`,
`aad`, `azure`, `azuredirectory`,
`live`, `liveconnect`, `liveid`|→|`azure-repos`|_N/A_ +`msa`, `microsoft`, `microsoftaccount`, `aad`, `azure`, `azuredirectory`, `live`, `liveconnect`, `liveid`|→|`azure-repos`|_N/A_ `github`|→|`github`|_N/A_ `basic`|→|`generic`|`false` -`integrated`, `windows`, `kerberos`, `ntlm`,
`tfs`, `sso`|→|`generic`|`true` _(default)_ +`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`.. diff --git a/docs/multiple-users.md b/docs/multiple-users.md index 20f1fd3cf..7446aef0e 100644 --- a/docs/multiple-users.md +++ b/docs/multiple-users.md @@ -10,11 +10,11 @@ Separate from the user strings in commits, Git recognizes the "user" part of a r 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! +## 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 identites on one domain, you'll need to insert a unique user part per-identity yourself. +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 distiguish 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. +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 diff --git a/docs/netconfig.md b/docs/netconfig.md index 7a6d5f51c..35cf9dc80 100644 --- a/docs/netconfig.md +++ b/docs/netconfig.md @@ -49,11 +49,11 @@ GCM supports other ways of configuring a proxy for convenience and compatibility 1. GCM-specific configuration options (_**only** respected by GCM; **deprecated**_): - `credential.httpProxy` - `credential.httpsProxy` -2. cURL environment variables (_also respected by Git_): +1. cURL environment variables (_also respected by Git_): - `http_proxy` - `https_proxy`/`HTTPS_PROXY` - `all_proxy`/`ALL_PROXY` -3. `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. @@ -76,7 +76,7 @@ addresses. GCM supports the cURL environment variable `no_proxy` (and 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 (`,`) or space (` `) separated +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), diff --git a/docs/usage.md b/docs/usage.md index bb0426e2b..1f64cbd1a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -31,7 +31,7 @@ Set your user-level Git configuration (`~/.gitconfig`) to use GCM. If you pass `--system` to these commands, they act on the system-level Git configuration (`/etc/gitconfig`) instead. -### azure-repos (experimental) +### 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. diff --git a/docs/windows-broker.md b/docs/windows-broker.md index 1ab1cc73b..62b422ee5 100644 --- a/docs/windows-broker.md +++ b/docs/windows-broker.md @@ -31,6 +31,7 @@ The GCM team isn't responsible for the user experience or choices made by WAM, b 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. 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. @@ -57,6 +58,7 @@ Similar to the above, your organization's Conditional Access policies may preven 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). #### 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**. @@ -65,6 +67,7 @@ In the section where you see your email address and organization name, click **D ![Disconnecting from Azure AD](img/aad-disconnect.png) ### 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. 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. @@ -81,8 +84,9 @@ For any connected MSA, you can control whether or not the account is available t ![Microsoft apps must ask to access your identity](img/apps-must-ask.png) 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). diff --git a/docs/wsl.md b/docs/wsl.md index 63b99e9ea..b5852f367 100644 --- a/docs/wsl.md +++ b/docs/wsl.md @@ -22,7 +22,7 @@ _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/libexec/git-core/git-credential-manager-core.exe" +git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager-core.exe" ``` If you intend to use Azure DevOps you must _also_ set the following Git @@ -102,7 +102,7 @@ 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-install-instructions). +To do this, simply follow the [GCM installation instructions for Linux](../README.md#linux). **Note:** In this scenario, because GCM is running as a Linux application it cannot utilize authentication or credential storage features of the host diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index 5250f4664..a9430850d 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -75,12 +75,12 @@ SYMBOLOUT="$PROJ_OUT/payload.sym/$CONFIGURATION" if [ $INSTALL_FROM_SOURCE = false ]; then TAROUT="$PROJ_OUT/tar/$CONFIGURATION" - TARBALL="$TAROUT/gcmcore-linux_$ARCH.$VERSION.tar.gz" - SYMTARBALL="$TAROUT/symbols-linux_$ARCH.$VERSION.tar.gz" + TARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION.tar.gz" + SYMTARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION-symbols.tar.gz" DEBOUT="$PROJ_OUT/deb/$CONFIGURATION" DEBROOT="$DEBOUT/root" - DEBPKG="$DEBOUT/gcmcore-linux_$ARCH.$VERSION.deb" + DEBPKG="$DEBOUT/gcm-linux_$ARCH.$VERSION.deb" else INSTALL_LOCATION="/usr/local" fi @@ -106,9 +106,13 @@ else mkdir -p "$INSTALL_LOCATION" fi +if [ -z "$DOTNET_ROOT" ]; then + DOTNET_ROOT="$(dirname $(which dotnet))" +fi + # Publish core application executables echo "Publishing core application..." -dotnet publish "$GCM_SRC" \ +$DOTNET_ROOT/dotnet publish "$GCM_SRC" \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -117,7 +121,7 @@ dotnet publish "$GCM_SRC" \ --output="$(make_absolute "$PAYLOAD")" || exit 1 echo "Publishing Bitbucket UI helper..." -dotnet publish "$BITBUCKET_UI_SRC" \ +$DOTNET_ROOT/dotnet publish "$BITBUCKET_UI_SRC" \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -126,7 +130,7 @@ dotnet publish "$BITBUCKET_UI_SRC" \ --output="$(make_absolute "$PAYLOAD")" || exit 1 echo "Publishing GitHub UI helper..." -dotnet publish "$GITHUB_UI_SRC" \ +$DOTNET_ROOT/dotnet publish "$GITHUB_UI_SRC" \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -135,7 +139,7 @@ dotnet publish "$GITHUB_UI_SRC" \ --output="$(make_absolute "$PAYLOAD")" || exit 1 echo "Publishing GitLab UI helper..." -dotnet publish "$GITLAB_UI_SRC" \ +$DOTNET_ROOT/dotnet publish "$GITLAB_UI_SRC" \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -189,7 +193,7 @@ if [ $INSTALL_FROM_SOURCE = false ]; then # https://stackoverflow.com/questions/9349616/bash-eof-in-if-statement # for details cat >"$DEBROOT/DEBIAN/control" < - - + + diff --git a/src/osx/Installer.Mac/build.sh b/src/osx/Installer.Mac/build.sh index ba5308da1..e52419601 100755 --- a/src/osx/Installer.Mac/build.sh +++ b/src/osx/Installer.Mac/build.sh @@ -22,6 +22,10 @@ case "$i" in CONFIGURATION="${i#*=}" shift # past argument=value ;; + --runtime=*) + RUNTIME="${i#*=}" + shift + ;; --version=*) VERSION="${i#*=}" shift # past argument=value @@ -38,15 +42,30 @@ if [ -z "$VERSION" ]; then die "--version was not set" fi +if [ -z "$RUNTIME" ]; then + TEST_RUNTIME=`uname -m` + case $TEST_RUNTIME in + "x86_64") + RUNTIME="osx-x64" + ;; + "arm64") + RUNTIME="osx-arm64" + ;; + *) + die "Unknown runtime '$TEST_RUNTIME'" + ;; + esac +fi + OUTDIR="$INSTALLER_OUT/pkg/$CONFIGURATION" PAYLOAD="$OUTDIR/payload" COMPONENTDIR="$OUTDIR/components" COMPONENTOUT="$COMPONENTDIR/com.microsoft.gitcredentialmanager.component.pkg" -DISTOUT="$OUTDIR/gcmcore-osx-$VERSION.pkg" +DISTOUT="$OUTDIR/gcm-osx-x64-$VERSION.pkg" # Layout and pack -"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --output="$PAYLOAD" || exit 1 +"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --output="$PAYLOAD" --runtime="$RUNTIME" || exit 1 "$INSTALLER_SRC/pack.sh" --payload="$PAYLOAD" --version="$VERSION" --output="$COMPONENTOUT" || exit 1 -"$INSTALLER_SRC/dist.sh" --package-path="$COMPONENTDIR" --version="$VERSION" --output="$DISTOUT" || exit 1 +"$INSTALLER_SRC/dist.sh" --package-path="$COMPONENTDIR" --version="$VERSION" --output="$DISTOUT" --runtime="$RUNTIME" || exit 1 echo "Build of Installer.Mac complete." diff --git a/src/osx/Installer.Mac/dist.sh b/src/osx/Installer.Mac/dist.sh index 749231583..c1f5b9328 100755 --- a/src/osx/Installer.Mac/dist.sh +++ b/src/osx/Installer.Mac/dist.sh @@ -11,7 +11,6 @@ SRC="$ROOT/src" OUT="$ROOT/out" INSTALLER_SRC="$SRC/osx/Installer.Mac" RESXPATH="$INSTALLER_SRC/resources" -DISTPATH="$INSTALLER_SRC/distribution.xml" # Product information IDENTIFIER="com.microsoft.gitcredentialmanager.dist" @@ -32,6 +31,10 @@ case "$i" in DISTOUT="${i#*=}" shift # past argument=value ;; + --runtime=*) + RUNTIME="${i#*=}" + shift + ;; *) # unknown option ;; @@ -50,6 +53,28 @@ fi if [ -z "$DISTOUT" ]; then die "--output was not set" fi +if [ -z "$RUNTIME" ]; then + TEST_RUNTIME=`uname -m` + case $TEST_RUNTIME in + "x86_64") + RUNTIME="osx-x64" + ;; + "arm64") + RUNTIME="osx-arm64" + ;; + *) + die "Unknown runtime '$TEST_RUNTIME'" + ;; + esac +fi + +echo "Building for runtime '$RUNTIME'" + +if [ "$RUNTIME" == "osx-x64"]; then + DISTPATH="$INSTALLER_SRC/distribution.x64.xml" +else + DISTPATH="$INSTALLER_SRC/distribution.arm64.xml" +fi # Cleanup any old package if [ -e "$DISTOUT" ]; then diff --git a/src/osx/Installer.Mac/distribution.xml b/src/osx/Installer.Mac/distribution.arm64.xml similarity index 93% rename from src/osx/Installer.Mac/distribution.xml rename to src/osx/Installer.Mac/distribution.arm64.xml index 657397513..531bdbe33 100644 --- a/src/osx/Installer.Mac/distribution.xml +++ b/src/osx/Installer.Mac/distribution.arm64.xml @@ -2,7 +2,7 @@ Git Credential Manager - + diff --git a/src/osx/Installer.Mac/distribution.x64.xml b/src/osx/Installer.Mac/distribution.x64.xml new file mode 100644 index 000000000..45deec220 --- /dev/null +++ b/src/osx/Installer.Mac/distribution.x64.xml @@ -0,0 +1,21 @@ + + + Git Credential Manager + + + + + + + + + + + + + + + + com.microsoft.gitcredentialmanager.component.pkg + + diff --git a/src/osx/Installer.Mac/entitlements.xml b/src/osx/Installer.Mac/entitlements.xml new file mode 100644 index 000000000..9acbcfa5c --- /dev/null +++ b/src/osx/Installer.Mac/entitlements.xml @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + \ No newline at end of file diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh index 288216165..b9991713d 100755 --- a/src/osx/Installer.Mac/layout.sh +++ b/src/osx/Installer.Mac/layout.sh @@ -27,7 +27,6 @@ GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia" # Build parameters FRAMEWORK=net6.0 -RUNTIME=osx-x64 # Parse script arguments for i in "$@" @@ -41,6 +40,10 @@ case "$i" in PAYLOAD="${i#*=}" shift # past argument=value ;; + --runtime=*) + RUNTIME="${i#*=}" + shift # past argument=value + ;; --symbol-output=*) SYMBOLOUT="${i#*=}" ;; @@ -50,6 +53,24 @@ case "$i" in esac done +# Determine a runtime if one was not provided +if [ -z "$RUNTIME" ]; then + TEST_RUNTIME=`uname -m` + case $TEST_RUNTIME in + "x86_64") + RUNTIME="osx-x64" + ;; + "arm64") + RUNTIME="osx-arm64" + ;; + *) + die "Unknown runtime '$TEST_RUNTIME'" + ;; + esac +fi + +echo "Building for runtime '$RUNTIME'" + # Perform pre-execution checks CONFIGURATION="${CONFIGURATION:=Debug}" if [ -z "$PAYLOAD" ]; then diff --git a/src/osx/Installer.Mac/uninstall.sh b/src/osx/Installer.Mac/uninstall.sh index 989ed9956..f26f2b189 100755 --- a/src/osx/Installer.Mac/uninstall.sh +++ b/src/osx/Installer.Mac/uninstall.sh @@ -12,7 +12,7 @@ fi # Unconfigure (as the current user) echo "Unconfiguring credential helper..." -sudo -u `/usr/bin/logname` "$GCMBIN" unconfigure +sudo -u `/usr/bin/logname` -E "$GCMBIN" unconfigure # Remove symlink if [ -L /usr/local/bin/git-credential-manager-core ] diff --git a/src/osx/SignFiles.Mac/SignFiles.Mac.csproj b/src/osx/SignFiles.Mac/SignFiles.Mac.csproj deleted file mode 100644 index 3de4601d9..000000000 --- a/src/osx/SignFiles.Mac/SignFiles.Mac.csproj +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - net6.0 - false - - $(RootDir) - $(BaseIntermediateOutputPath)\tmp\macsign - 8003 - - - - - - all - - - - - - Microsoft400 - false - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/osx/SignFiles.Mac/notarize-pkg.sh b/src/osx/SignFiles.Mac/notarize-pkg.sh deleted file mode 100755 index e7566b851..000000000 --- a/src/osx/SignFiles.Mac/notarize-pkg.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/bin/bash - -# This file was based on https://github.com/microsoft/BuildXL/blob/8c2348ff04e6ca78726bb945fb2a0f6a55a5c7d6/Private/macOS/notarize.sh -# -# For detailed explanation see: https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution/customizing_the_notarization_workflow - -usage() { - cat < -p -pkg - -id or --appleid # A valid Apple ID email address, account must have correct certificates available - -p or --password # The password for the specified Apple ID or Apple One-Time password (to avoid 2FA) - -pkg or --package # The path to an already signed flat-package -EOM - exit 0 -} - -declare arg_AppleId="" -declare arg_Password="" -declare arg_PackagePath="" - -[ $# -eq 0 ] && { usage; } - -function parseArgs() { - arg_Positional=() - while [[ $# -gt 0 ]]; do - cmd="$1" - case $cmd in - --help | -h) - usage - shift - exit 0 - ;; - --appleid | -id) - arg_AppleId=$2 - shift - ;; - --password | -p) - arg_Password="$2" - shift - ;; - --package | -pkg) - arg_PackagePath="$2" - shift - ;; - *) - arg_Positional+=("$1") - shift - ;; - esac - done -} - -function getPackageId { - local PKG=$(cd "$(dirname "$1")"; pwd)/$(basename "$1") - local PKGDEST=$(mktemp -d | tr -d '\r') - xar -x -f "${PKG}" --exclude '^(?:(?!PackageInfo).)*$' -C "${PKGDEST}" - if [ ! -e "${PKGDEST}/PackageInfo" ]; then - echo "error: can't find 'PackageInfo'; maybe meta-package" - return 1 - fi - cat "${PKGDEST}/PackageInfo" | tr -d '\r' | tr -d '\n' | sed 's:^.*identifier="\([^"]*\)".*$:\1:g' - rm -rf "${PKGDEST}" -} - -parseArgs "$@" - -if [[ -z $arg_AppleId ]]; then - echo "[ERROR] Must supply valid / non-empty Apple ID!" - exit 1 -fi - -if [[ -z $arg_Password ]]; then - echo "[ERROR] Must supply valid / non-empty password!" - exit 1 -fi - -if [[ ! -f $arg_PackagePath ]]; then - echo "[ERROR] Must supply valid / non-empty path to package!" - exit 1 -fi - -declare bundle_id=$(getPackageId ${arg_PackagePath}) - -if [[ -z $bundle_id ]]; then - echo "[ERROR] No identifier found in package info!" - exit 1 -fi - -echo "Notarizating $arg_PackagePath" - -echo -e "Current state:\n" -xcrun stapler validate -v "$arg_PackagePath" - -if [[ $? -eq 0 ]]; then - echo "$arg_PackagePath already notarized and stapled, nothing to do!" - exit 0 -fi - -set -e - -declare start_time=$(date +%s) - -declare output="/tmp/progress.xml" - -echo "Uploading package to notarization service, please wait..." -xcrun altool --notarize-app -t osx -f $arg_PackagePath --primary-bundle-id $bundle_id -u $arg_AppleId -p $arg_Password --output-format xml | tee $output - -declare request_id=$(/usr/libexec/PlistBuddy -c "print :notarization-upload:RequestUUID" $output) - -echo "Checking notarization request validity..." -if [[ $request_id =~ ^\{?[A-F0-9a-f]{8}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{12}\}?$ ]]; then - declare attempts=5 - - while : - do - echo "Waiting a bit before checking on notarization status again..." - - sleep 20 - xcrun altool --notarization-info $request_id -u $arg_AppleId -p $arg_Password --output-format xml | tee $output - - declare status=$(/usr/libexec/PlistBuddy -c "print :notarization-info:Status" $output) - echo "Status: $status" - - if [[ -z $status ]]; then - echo "Left attempts: $attempts" - - if (($attempts <= 0)); then - break - fi - - ((attempts--)) - else - if [[ $status != "in progress" ]]; then - break - fi - fi - done - - declare end_time=$(date +%s) - echo -e "Completed in $(($end_time-$start_time)) seconds\n" - - if [[ "$status" != "success" ]]; then - echo "Error notarizing, exiting..." >&2 - exit 1 - else - declare url=$(/usr/libexec/PlistBuddy -c "print :notarization-info:LogFileURL" $output) - - if [ "$url" ]; then - curl $url - fi - - # Staple the ticket to the package - xcrun stapler staple "$arg_PackagePath" - - echo -e "State after notarization:\n" - xcrun stapler validate -v "$arg_PackagePath" - echo -e "Stapler exit code: $? (must be zero on success!)\n" - fi -else - echo "Invalid request id found in 'altool' output, aborting!" >&2 - exit 1 -fi diff --git a/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj b/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj index 61f8f2ae5..18d7f90d7 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj +++ b/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj @@ -12,7 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs index 708d1205a..aace44ad7 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs @@ -1,6 +1,10 @@ using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using GitCredentialManager; using GitCredentialManager.Tests.Objects; +using Moq; using Xunit; namespace Atlassian.Bitbucket.Tests @@ -105,18 +109,111 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_All_NoDesktopSessi } [Fact] - public async Task BitbucketAuthentication_ShowOAuthRequiredPromptAsync_SucceedsAfterUserInput() + public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBCloud_HelperCmdLine() { + var targetUri = new Uri("https://bitbucket.org"); + + var helperPath = "/usr/bin/test-helper"; + var expectedUserName = "jsquire"; + var expectedPassword = "password"; + var resultDict = new Dictionary + { + ["username"] = expectedUserName, + ["password"] = expectedPassword + }; + + string expectedArgs = $"prompt --show-basic --show-oauth"; + var context = new TestCommandContext(); - context.Terminal.Prompts["Press enter to continue..."] = " "; + context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection - var bitbucketAuthentication = new BitbucketAuthentication(context); + var authMock = new Mock(context) { CallBase = true }; + authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath)) + .Returns(true); + authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None)) + .ReturnsAsync(resultDict); + + BitbucketAuthentication auth = authMock.Object; + CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All); + + Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode); + Assert.Equal(result.Credential.Account, expectedUserName); + Assert.Equal(result.Credential.Password, expectedPassword); + + authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None), + Times.Once); + } + + [Fact] + public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBCloud_HelperCmdLine() + { + var targetUri = new Uri("https://bitbucket.org"); + + var helperPath = "/usr/bin/test-helper"; + var expectedUserName = "jsquire"; + var expectedPassword = "password"; + var resultDict = new Dictionary + { + ["username"] = expectedUserName, + ["password"] = expectedPassword + }; + + string expectedArgs = $"prompt --username {expectedUserName} --show-basic"; + + var context = new TestCommandContext(); + context.SessionManager.IsDesktopSession = true; // Enable UI helper selection - var result = await bitbucketAuthentication.ShowOAuthRequiredPromptAsync(); + var authMock = new Mock(context) { CallBase = true }; + authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath)) + .Returns(true); + authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None)) + .ReturnsAsync(resultDict); + + BitbucketAuthentication auth = authMock.Object; + CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, expectedUserName, AuthenticationModes.Basic); + + Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode); + Assert.Equal(result.Credential.Account, expectedUserName); + Assert.Equal(result.Credential.Password, expectedPassword); + + authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None), + Times.Once); + } + + [Fact] + public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBServerDC_HelperCmdLine() + { + var targetUri = new Uri("https://example.com/bitbucket"); + + var helperPath = "/usr/bin/test-helper"; + var expectedUserName = "jsquire"; + var expectedPassword = "password"; + var resultDict = new Dictionary + { + ["username"] = expectedUserName, + ["password"] = expectedPassword + }; + + string expectedArgs = $"prompt --url {targetUri} --show-basic --show-oauth"; + + var context = new TestCommandContext(); + context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection + + var authMock = new Mock(context) { CallBase = true }; + authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath)) + .Returns(true); + authMock.Setup(x => x.InvokeHelperAsync(It.IsAny(), It.IsAny(), null, CancellationToken.None)) + .ReturnsAsync(resultDict); + + BitbucketAuthentication auth = authMock.Object; + CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All); + + Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode); + Assert.Equal(result.Credential.Account, expectedUserName); + Assert.Equal(result.Credential.Password, expectedPassword); - Assert.True(result); - Assert.Equal($"Your account has two-factor authentication enabled.{Environment.NewLine}" + - $"To continue you must complete authentication in your web browser.{Environment.NewLine}", context.Terminal.Messages[0].Item1); + authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None), + Times.Once); } } } diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index 6afc52f45..cf8216afc 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -15,12 +15,66 @@ public class BitbucketHostProviderTest #region Tests private const string MOCK_ACCESS_TOKEN = "at-0987654321"; + private const string MOCK_ACCESS_TOKEN_ALT = "at-onetwothreefour-1234"; + private const string MOCK_EXPIRED_ACCESS_TOKEN = "at-1234567890-expired"; private const string MOCK_REFRESH_TOKEN = "rt-1234567809"; private const string BITBUCKET_DOT_ORG_HOST = "bitbucket.org"; private const string DC_SERVER_HOST = "example.com"; 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 @@ -86,210 +140,262 @@ public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header, [Theory] [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredBasicAuthAccount(string protocol, string host, string username,string password) + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_Basic( + string protocol, string host, string username, string password) { InputArguments input = MockInput(protocol, host, username); var context = new TestCommandContext(); MockStoredAccount(context, input, password); - MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password); + MockRemoteBasicValid(input, password); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); - //verify bitbucket.org credentials were validated + Assert.Equal(username, credential.Account); + Assert.Equal(password, credential.Password); + + // Verify bitbucket.org credentials were validated if (BITBUCKET_DOT_ORG_HOST.Equals(host)) { - VerifyValidateBasicAuthCredentialsRan(); + VerifyValidateBasicAuthCredentialsRan(input, password); } else { - //verify DC/Server credentials were not validated + // Verify DC/Server credentials were not validated VerifyValidateBasicAuthCredentialsNeverRan(); } // Stored credentials so don't ask for more - VerifyInteractiveBasicAuthFlowNeverRan(password, input, credential); - - // Valid Basic Auth credentials so don't run Oauth - VerifyInteractiveOAuthFlowNeverRan(input, credential); + VerifyInteractiveAuthNeverRan(); } [Theory] // DC/Server does not currently support OAuth [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForValidStoredOAuthAccount(string protocol, string host, string username,string token) + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth( + string protocol, string host, string username, string token) { InputArguments input = MockInput(protocol, host, username); var context = new TestCommandContext(); MockStoredAccount(context, input, token); - MockRemoteOAuthAccountIsValid(bitbucketApi, input, token, false); + MockRemoteAccessTokenValid(input, token); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); - //verify bitbucket.org credentials were validated - VerifyValidateOAuthCredentialsRan(); + Assert.Equal(username, credential.Account); + Assert.Equal(token, credential.Password); - // Stored credentials so don't ask for more - VerifyInteractiveBasicAuthFlowNeverRan(token, input, credential); + // Verify bitbucket.org credentials were validated + VerifyValidateAccessTokenRan(input, token); - // Valid Basic Auth credentials so don't run Oauth - VerifyInteractiveOAuthFlowNeverRan(input, credential); + // Stored credentials so don't ask for more + VerifyInteractiveAuthNeverRan(); } [Theory] // DC [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] - // cloud + // Cloud [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValidBasicAuthAccount(string protocol, string host, string username, string password) + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_Basic( + string protocol, string host, string username, string password) { InputArguments input = MockInput(protocol, host, username); var context = new TestCommandContext(); - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); + MockPromptBasic(input, password); + MockRemoteBasicValid(input, password); - if (BITBUCKET_DOT_ORG_HOST.Equals(host)) - { - MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); - } + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + + var credential = await provider.GetCredentialAsync(input); + + Assert.Equal(username, credential.Account); + Assert.Equal(password, credential.Password); + + VerifyInteractiveAuthRan(input); + } + + [Theory] + // DC/Server does not currently support OAuth + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_OAuth( + string protocol, string host, string username, string refreshToken, string accessToken) + { + InputArguments input = MockInput(protocol, host, username); - MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password); + var context = new TestCommandContext(); + + MockPromptOAuth(input); + MockRemoteOAuthTokenCreate(input, accessToken, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); - VerifyBasicAuthFlowRan(password, true, input, credential, null); + Assert.Equal(username, credential.Account); + Assert.Equal(accessToken, credential.Password); - VerifyOAuthFlowDidNotRun(password, true, input, credential); + VerifyInteractiveAuthRan(input); + VerifyOAuthFlowRan(input, accessToken); + VerifyValidateAccessTokenRan(input, accessToken); + VerifyOAuthRefreshTokenStored(context, input, refreshToken); } [Theory] // DC/Server does not currently support OAuth - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_ACCESS_TOKEN)] - public void BitbucketHostProvider_GetCredentialAsync_Succeeds_ForFreshValid2FAAcccount(string protocol, string host, string username, string password) + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_MissingAT_OAuth_Refresh( + string protocol, string host, string username, string refreshToken, string accessToken) { var input = MockInput(protocol, host, username); var context = new TestCommandContext(); - // user is prompted for basic auth credentials - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - // basic auth credentials are valid but 2FA is ON - MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password); - MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); - MockRemoteValidRefreshToken(); + // AT has does not exist, but RT is still valid + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); + MockRemoteRefreshTokenValid(refreshToken, accessToken); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); - VerifyOAuthFlowRan(password, false, true, input, credential, null); + Assert.Equal(username, credential.Account); + Assert.Equal(accessToken, credential.Password); - VerifyBasicAuthFlowNeverRan(password, input, false, null); + VerifyValidateAccessTokenRan(input, accessToken); + VerifyOAuthRefreshRan(refreshToken); + VerifyInteractiveAuthNeverRan(); } [Theory] - // cloud - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "basic")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "oauth")] - // Basic Auth works - public void BitbucketHostProvider_GetCredentialAsync_ForcedAuthMode_IsRespected(string protocol, string host, string username, string password, - string preconfiguredAuthModes) + // DC/Server does not currently support OAuth + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_EXPIRED_ACCESS_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_ExpiredAT_OAuth_Refresh( + string protocol, string host, string username, string refreshToken, string expiredAccessToken, string accessToken) { var input = MockInput(protocol, host, username); var context = new TestCommandContext(); - if (preconfiguredAuthModes != null) - { - context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, preconfiguredAuthModes); - } - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password); - bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); + // AT exists but has expired, but RT is still valid + MockStoredAccount(context, input, expiredAccessToken); + MockRemoteAccessTokenExpired(input, expiredAccessToken); + + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); + MockRemoteRefreshTokenValid(refreshToken, accessToken); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); + + Assert.Equal(username, credential.Account); + Assert.Equal(accessToken, credential.Password); + + VerifyValidateAccessTokenRan(input, accessToken); + VerifyOAuthRefreshRan(refreshToken); + VerifyInteractiveAuthNeverRan(); + } + + [Theory] + // Cloud + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", MOCK_REFRESH_TOKEN, MOCK_ACCESS_TOKEN)] + public async Task BitbucketHostProvider_GetCredentialAsync_PreconfiguredMode_OAuth_ValidRT_IsRespected( + string protocol, string host, string username, string refreshToken, string accessToken) + { + var input = MockInput(protocol, host, username); + + var context = new TestCommandContext(); + context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, "oauth"); + + // We have a stored RT so we can just use that without any prompts + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, accessToken); + MockRemoteRefreshTokenValid(refreshToken, accessToken); + + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + + var credential = await provider.GetCredentialAsync(input); Assert.NotNull(credential); - if (preconfiguredAuthModes.Contains("basic")) - { - VerifyInteractiveBasicAuthFlowRan(password, input, credential); - VerifyInteractiveOAuthFlowNeverRan(input, credential); - } + VerifyInteractiveAuthNeverRan(); + VerifyOAuthRefreshRan(refreshToken); + } - if (preconfiguredAuthModes.Contains("oauth")) - { - VerifyInteractiveBasicAuthFlowNeverRan(password, input, credential); - VerifyInteractiveOAuthFlowRan(password, input, credential); - } + [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) + { + var input = MockInput(protocol, host, username); + + var context = new TestCommandContext(); + context.Environment.Variables.Add( + BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString); + + // User has stored access token that we shouldn't use - RT should be used to mint new AT + MockStoredAccount(context, input, storedToken); + MockStoredRefreshToken(context, input, refreshToken); + MockRemoteAccessTokenValid(input, newToken); + MockRemoteRefreshTokenValid(refreshToken, newToken); + + var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); + + var credential = await provider.GetCredentialAsync(input); + + Assert.Equal(username, credential.Account); + Assert.Equal(newToken, credential.Password); + + VerifyInteractiveAuthNeverRan(); + VerifyOAuthRefreshRan(refreshToken); } [Theory] - // cloud - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "false")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "0")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "true")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", "1")] - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password", null)] + // Cloud + [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "old-password", "new-password")] // DC - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "false")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "0")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "1")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", "true")] - [InlineData("https", DC_SERVER_HOST, "jsquire", "password", null)] - public void BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_IsRespected(string protocol, string host, string username, string password, - string alwaysRefreshCredentials) + [InlineData("https", DC_SERVER_HOST, "jsquire", "old-password", "new-password")] + public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredentials_Basic_IsRespected( + string protocol, string host, string username, string storedPassword, string freshPassword) { var input = MockInput(protocol, host, username); var context = new TestCommandContext(); - if (alwaysRefreshCredentials != null) - { - context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, alwaysRefreshCredentials); - } + context.Environment.Variables.Add( + BitbucketConstants.EnvironmentVariables.AlwaysRefreshCredentials, bool.TrueString); - MockStoredAccount(context, input, password); - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteOAuthAccountIsValid(bitbucketApi, input, password, true); - MockRemoteBasicAuthAccountIsValidNo2FA(bitbucketApi, input, password); + // User has stored password that we shouldn't use + MockStoredAccount(context, input, storedPassword); + MockPromptBasic(input, freshPassword); var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - var credential = provider.GetCredentialAsync(input); + var credential = await provider.GetCredentialAsync(input); - var alwaysRefreshCredentialsBool = "1".Equals(alwaysRefreshCredentials) - || "on".Equals(alwaysRefreshCredentials) - || "true".Equals(alwaysRefreshCredentials) ? true : false; + Assert.Equal(username, credential.Account); + Assert.Equal(freshPassword, credential.Password); - if (alwaysRefreshCredentialsBool) - { - VerifyBasicAuthFlowRan(password, true, input, credential, null); - } - else - { - VerifyBasicAuthFlowNeverRan(password, input, true, null); - } - - VerifyOAuthFlowDidNotRun(password, true, input, credential); + VerifyInteractiveAuthRan(input); } [Theory] // DC - supports Basic [InlineData("https://example.com", "basic", AuthenticationModes.Basic)] [InlineData("https://example.com", "oauth", AuthenticationModes.Basic)] - // cloud - supports Basic, OAuth + // 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)] @@ -302,7 +408,7 @@ public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriStri { var targetUri = new Uri(uriString); - var context = new TestCommandContext { }; + var context = new TestCommandContext(); if (bitbucketAuthModes != null) { context.Environment.Variables.Add(BitbucketConstants.EnvironmentVariables.AuthenticationModes, bitbucketAuthModes); @@ -315,40 +421,6 @@ public void BitbucketHostProvider_GetSupportedAuthenticationModes(string uriStri Assert.Equal(expectedModes, actualModes); } - [Theory] - // DC - [InlineData("https", DC_SERVER_HOST, "jsquire", "password")] - [InlineData("http", DC_SERVER_HOST, "jsquire", "password")] - // cloud - [InlineData("https", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - [InlineData("http", BITBUCKET_DOT_ORG_HOST, "jsquire", "password")] - public async Task BitbucketHostProvider_GetCredentialAsync_ValidateTargetUriAsync(string protocol, string host, string username, string password) - { - var input = MockInput(protocol, host, username); - - var context = new TestCommandContext(); - - var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, bitbucketApi.Object); - - if (protocol.ToLower().Equals("http") && host.ToLower().Equals(BITBUCKET_DOT_ORG_HOST)) - { - // only fail for http://bitbucket.org - await Assert.ThrowsAsync(async () => await provider.GetCredentialAsync(input)); - } - else - { - MockUserEntersValidBasicCredentials(bitbucketAuthentication, input, password); - MockRemoteBasicAuthAccountIsValidRequires2FA(bitbucketApi, input, password); - MockRemoteValidRefreshToken(); - bitbucketAuthentication.Setup(m => m.ShowOAuthRequiredPromptAsync()).ReturnsAsync(true); - bitbucketAuthentication.Setup(m => m.CreateOAuthCredentialsAsync(It.IsAny())).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = false }; - bitbucketApi.Setup(x => x.GetUserInformationAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); - - var credential = await provider.GetCredentialAsync(input); - } - } - [Theory] [InlineData("https", DC_SERVER_HOST, "jsquire")] public async Task BitbucketHostProvider_StoreCredentialAsync(string protocol, string host, string username) @@ -388,6 +460,7 @@ public async Task BitbucketHostProvider_EraseCredentialAsync(string protocol, st #endregion #region Test helpers + private static InputArguments MockInput(string protocol, string host, string username) { return new InputArguments(new Dictionary @@ -398,224 +471,102 @@ private static InputArguments MockInput(string protocol, string host, string use }); } - private void VerifyBasicAuthFlowRan(string password, bool expected, InputArguments input, Task credential, - string preconfiguredAuthModes) + private void VerifyOAuthFlowRan(InputArguments input, string token) { - Assert.Equal(expected, credential != null); - var remoteUri = input.GetRemoteUri(); - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); + // Get new access token and refresh token + bitbucketAuthentication.Verify(m => m.CreateOAuthCredentialsAsync(remoteUri), Times.Once); - // check username/password for Bitbucket.org - if ((preconfiguredAuthModes == null && BITBUCKET_DOT_ORG_HOST == remoteUri.Host) - || (preconfiguredAuthModes != null && preconfiguredAuthModes.Contains("oauth"))) - { - bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once); - } + // Check access token works/resolve username + bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once); } - private void VerifyInteractiveBasicAuthFlowRan(string password, InputArguments input, Task credential) - { - var remoteUri = input.GetRemoteUri(); - - // verify users was prompted for username/password credentials - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); - - // check username/password for Bitbucket.org - if (BITBUCKET_DOT_ORG_HOST == remoteUri.Host) - { - bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once); - } - } - - private void VerifyBasicAuthFlowNeverRan(string password, InputArguments input, bool storedAccount, - string preconfiguredAuthModes) - { - var remoteUri = input.GetRemoteUri(); - - if (!storedAccount && - (preconfiguredAuthModes == null || preconfiguredAuthModes.Contains("basic")) ) - { - // never prompt the user for basic credentials - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); - } - else - { - // never prompt the user for basic credentials - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Never); - } - } - - private void VerifyInteractiveBasicAuthFlowNeverRan(string password, InputArguments input, Task credential) - { - var remoteUri = input.GetRemoteUri(); - - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Never); - } - - private void VerifyOAuthFlowRan(string password, bool storedAccount, bool expected, InputArguments input, Task credential, - string preconfiguredAuthModes) + private void VerifyValidateBasicAuthCredentialsNeverRan() { - Assert.Equal(expected, credential != null); - - var remoteUri = input.GetRemoteUri(); - - if (storedAccount) - { - // use refresh token to get new access token and refresh token - bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN), Times.Once); - - // check access token works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Once); - } - else - { - if (preconfiguredAuthModes == null || preconfiguredAuthModes.Contains("basic")) - { - // prompt user for basic auth, if basic auth is not excluded - bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); - - // check if entered Basic Auth credentials work, if basic auth is not excluded - bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once); - } - - // Basic Auth 403-ed so push user through OAuth flow - bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Once); - } + // Never check username/password works + bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Never); } - private void VerifyInteractiveOAuthFlowRan(string password, InputArguments input, System.Threading.Tasks.Task credential) + private void VerifyValidateBasicAuthCredentialsRan(InputArguments input, string password) { - var remoteUri = input.GetRemoteUri(); - - // Basic Auth 403-ed so push user through OAuth flow - bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Once); - + // Check username/password works + bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once); } - private void VerifyOAuthFlowDidNotRun(string password, bool expected, InputArguments input, System.Threading.Tasks.Task credential) + private void VerifyValidateAccessTokenRan(InputArguments input, string token) { - Assert.Equal(expected, credential != null); - - var remoteUri = input.GetRemoteUri(); - - // never prompt user through OAuth flow - bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Never); - - // Never try to refresh Access Token - bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(It.IsAny()), Times.Never); - - // never check access token works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never); + // Check tokens works + bitbucketApi.Verify(m => m.GetUserInformationAsync(null, token, true), Times.Once); } - private void VerifyInteractiveOAuthFlowNeverRan(InputArguments input, System.Threading.Tasks.Task credential) + private void VerifyInteractiveAuthRan(InputArguments input) { var remoteUri = input.GetRemoteUri(); - // never prompt user through OAuth flow - bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Never); - - // Never try to refresh Access Token - bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(It.IsAny()), Times.Never); - - // never check access token works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never); - } - - private void VerifyValidateBasicAuthCredentialsNeverRan() - { - // never check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Never); - } - - private void VerifyValidateBasicAuthCredentialsRan() - { - // check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(It.IsAny(), It.IsAny(), false), Times.Once); - } - - private void VerifyValidateOAuthCredentialsNeverRan() - { - // never check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, It.IsAny(), false), Times.Never); + bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny()), Times.Once); } - private void VerifyValidateOAuthCredentialsRan() + private void VerifyInteractiveAuthNeverRan() { - // check username/password works - bitbucketApi.Verify(m => m.GetUserInformationAsync(null, It.IsAny(), true), Times.Once); + bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } - private void MockStoredOAuthAccount(TestCommandContext context, InputArguments input) + private void VerifyOAuthRefreshRan(string refreshToken) { - // refresh token - context.CredentialStore.Add("https://bitbucket.org/refresh_token", new TestCredential(input.Host, input.UserName, MOCK_REFRESH_TOKEN)); - // auth token - context.CredentialStore.Add("https://bitbucket.org", new TestCredential(input.Host, input.UserName, MOCK_ACCESS_TOKEN)); + // Check refresh was called + bitbucketAuthentication.Verify(m => m.RefreshOAuthCredentialsAsync(refreshToken), Times.Once); } - private void MockRemoteValidRefreshToken() + private void MockRemoteRefreshTokenValid(string refreshToken, string accessToken) { - bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(MOCK_REFRESH_TOKEN)).ReturnsAsync(new OAuth2TokenResult(MOCK_ACCESS_TOKEN, "access_token")); + bitbucketAuthentication.Setup(m => m.RefreshOAuthCredentialsAsync(refreshToken)).ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token")); } - private static void MockInvalidRemoteBasicAccount(Mock bitbucketApi, Mock bitbucketAuthentication) - { - bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, null)); - - bitbucketApi.Setup(x => x.GetUserInformationAsync(It.IsAny(), It.IsAny(), false)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized)); - - } - private static void MockUserEntersValidBasicCredentials(Mock bitbucketAuthentication, InputArguments input, string password) + private void MockPromptBasic(InputArguments input, string password) { var remoteUri = input.GetRemoteUri(); bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny())) .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, new TestCredential(input.Host, input.UserName, password))); } - private static void MockUserDoesNotEntersValidBasicCredentials(Mock bitbucketAuthentication) + private void MockPromptOAuth(InputArguments input) { - bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, null)); + var remoteUri = input.GetRemoteUri(); + bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny())) + .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.OAuth)); } - private static void MockRemoteBasicAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled) + private void MockRemoteBasicValid(InputArguments input, string password, bool twoFactor = true) { - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled }; + var userInfo = new UserInfo + { + UserName = input.UserName, + IsTwoFactorAuthenticationEnabled = twoFactor + }; + // Basic bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false)) .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); - } - private static void MockRemoteBasicAuthAccountIsValidRequires2FA(Mock bitbucketApi, InputArguments input, string password) + private void MockRemoteAccessTokenExpired(InputArguments input, string token) { - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, true); - } - - private static void MockRemoteBasicAuthAccountIsValidNo2FA(Mock bitbucketApi, InputArguments input, string password) - { - MockRemoteBasicAuthAccountIsValid(bitbucketApi, input, password, false); + // OAuth + bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true)) + .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Unauthorized)); } - private static void MockRemoteBasicAuthAccountIsInvalid(Mock bitbucketApi, InputArguments input, string password) + private void MockRemoteAccessTokenValid(InputArguments input, string token, bool twoFactor = true) { - var userInfo = new UserInfo(); - // Basic - bitbucketApi.Setup(x => x.GetUserInformationAsync(input.UserName, password, false)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.Forbidden, userInfo)); - - } + var userInfo = new UserInfo + { + UserName = input.UserName, + IsTwoFactorAuthenticationEnabled = twoFactor + }; - private static void MockRemoteOAuthAccountIsValid(Mock bitbucketApi, InputArguments input, string password, bool twoFAEnabled) - { - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = twoFAEnabled }; // OAuth - bitbucketApi.Setup(x => x.GetUserInformationAsync(null, password, true)) + bitbucketApi.Setup(x => x.GetUserInformationAsync(null, token, true)) .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); } @@ -626,12 +577,28 @@ private static void MockStoredAccount(TestCommandContext context, InputArguments context.CredentialStore.Add(remoteUrl, new TestCredential(input.Host, input.UserName, password)); } - private static void MockValidStoredOAuthUser(TestCommandContext context, Mock bitbucketApi) + private static void MockStoredRefreshToken(TestCommandContext context, InputArguments input, string token) { - var userInfo = new UserInfo() { IsTwoFactorAuthenticationEnabled = false }; - bitbucketApi.Setup(x => x.GetUserInformationAsync("jsquire", "password1", false)) - .ReturnsAsync(new RestApiResult(System.Net.HttpStatusCode.OK, userInfo)); - context.CredentialStore.Add("https://bitbucket.org", new TestCredential("https://bitbucket.org", "jsquire", "password1")); + var remoteUri = input.GetRemoteUri(); + var refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri); + context.CredentialStore.Add(refreshService, new TestCredential(refreshService, input.UserName, token)); + } + + private void MockRemoteOAuthTokenCreate(InputArguments input, string accessToken, string refreshToken) + { + var remoteUri = input.GetRemoteUri(); + bitbucketAuthentication.Setup(x => x.CreateOAuthCredentialsAsync(remoteUri)) + .ReturnsAsync(new OAuth2TokenResult(accessToken, "access_token") { RefreshToken = refreshToken }); + } + + private void VerifyOAuthRefreshTokenStored(TestCommandContext context, InputArguments input, string refreshToken) + { + var remoteUri = input.GetRemoteUri(); + string refreshService = BitbucketHostProvider.GetRefreshTokenServiceName(remoteUri); + bool result = context.CredentialStore.TryGet(refreshService, input.UserName, out var credential); + + Assert.True(result); + Assert.Equal(refreshToken, credential.Password); } #endregion diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj index e1ae6d4af..a9185278a 100644 --- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj +++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj @@ -3,7 +3,7 @@ WinExe net6.0 - osx-x64;linux-x64 + osx-x64;linux-x64;osx-arm64 Atlassian.Bitbucket.UI Atlassian.Bitbucket.UI diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs deleted file mode 100644 index f3ee88591..000000000 --- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/OAuthCommandImpl.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Atlassian.Bitbucket.UI.ViewModels; -using Atlassian.Bitbucket.UI.Views; -using GitCredentialManager; -using GitCredentialManager.UI; - -namespace Atlassian.Bitbucket.UI.Commands -{ - public class OAuthCommandImpl : OAuthCommand - { - public OAuthCommandImpl(CommandContext context) : base(context) { } - - protected override Task ShowAsync(OAuthViewModel viewModel, CancellationToken ct) - { - return AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), ct); - } - } -} diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml index 86b010696..fca8daf48 100644 --- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml +++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml @@ -6,8 +6,24 @@ x:Class="Atlassian.Bitbucket.UI.Controls.TesterWindow" Title="Bitbucket Authentication Dialog Tester" Height="240" Width="420" CanResize="False"> - -