From 446dda91f753dca01e52b2f1c2b313a8d6dedc47 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Mon, 21 Jun 2021 12:56:40 -0700 Subject: [PATCH 01/19] !fixup winget tag specification --- .github/workflows/release-winget.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-winget.yaml b/.github/workflows/release-winget.yaml index 5783f6d0f..b91ed74cf 100644 --- a/.github/workflows/release-winget.yaml +++ b/.github/workflows/release-winget.yaml @@ -21,7 +21,11 @@ jobs: Publisher: Microsoft Corporation Moniker: git-credential-manager-core PackageUrl: https://aka.ms/gcmcore - Tags: [ gcm, gcmcore, git, credential ] + Tags: + - gcm + - gcmcore + - git + - credential License: Copyright (C) Microsoft Corporation ShortDescription: Secure, cross-platform Git credential storage with authentication to GitHub, Azure Repos, and other popular Git hosting services. Installers: From 690d8fcc432d3c8c8e9bca45ab58e49afe7808c7 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 22 Jun 2021 15:31:11 +0100 Subject: [PATCH 02/19] osx: avoid extra restore in macOS installer creation Avoid an extra `dotnet restore` during the `dotnet publish` commands in `layout.sh` when building the macOS installer. For some reason, the CI machines (and sometimes locally) hang during this step with: ``` Building Installer.Mac Copying uninstall script... Microsoft (R) Build Engine version 16.10.0-preview-21181-07+073022eb4 for .NET Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... ``` Avoiding the restore during publish should be OK since the hosting Installer.Mac.csproj project will have already built (and restored) packages for the dependent projects. --- src/osx/Installer.Mac/layout.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh index 9d4493e66..1c4d1e6b6 100755 --- a/src/osx/Installer.Mac/layout.sh +++ b/src/osx/Installer.Mac/layout.sh @@ -74,6 +74,7 @@ cp "$INSTALLER_SRC/uninstall.sh" "$PAYLOAD" || exit 1 # Publish core application executables echo "Publishing core application..." dotnet publish "$GCM_SRC" \ + --no-restore \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -81,6 +82,7 @@ dotnet publish "$GCM_SRC" \ echo "Publishing Bitbucket UI helper..." dotnet publish "$BITBUCKET_UI_SRC" \ + --no-restore \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -88,6 +90,7 @@ dotnet publish "$BITBUCKET_UI_SRC" \ echo "Publishing GitHub UI helper..." dotnet publish "$GITHUB_UI_SRC" \ + --no-restore \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ From 0252d800f0eacbe9928549da30d372d9d0235b7f Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 22 Jun 2021 16:06:44 +0100 Subject: [PATCH 03/19] osx: only publish using one MSBuild node --- src/osx/Installer.Mac/layout.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh index 1c4d1e6b6..898450963 100755 --- a/src/osx/Installer.Mac/layout.sh +++ b/src/osx/Installer.Mac/layout.sh @@ -75,6 +75,7 @@ cp "$INSTALLER_SRC/uninstall.sh" "$PAYLOAD" || exit 1 echo "Publishing core application..." dotnet publish "$GCM_SRC" \ --no-restore \ + -m:1 \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -83,6 +84,7 @@ dotnet publish "$GCM_SRC" \ echo "Publishing Bitbucket UI helper..." dotnet publish "$BITBUCKET_UI_SRC" \ --no-restore \ + -m:1 \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ @@ -91,6 +93,7 @@ dotnet publish "$BITBUCKET_UI_SRC" \ echo "Publishing GitHub UI helper..." dotnet publish "$GITHUB_UI_SRC" \ --no-restore \ + -m:1 \ --configuration="$CONFIGURATION" \ --framework="$FRAMEWORK" \ --runtime="$RUNTIME" \ From 702ddb3d2068f3da6bf3711b6286464a620412e2 Mon Sep 17 00:00:00 2001 From: Mike Minns Date: Fri, 18 Jun 2021 11:35:42 +0100 Subject: [PATCH 04/19] Issue-267 Add fingerprint header, to autodetect Bitbucket DC instances. Copied and refreshed the Bitbucket development doc from the Git Credential Manager for Windows project. --- docs/bitbucket-development.md | 80 +++++++++++++++++++ .../BitbucketHostProviderTest.cs | 60 ++++++++++++++ .../BitbucketHostProvider.cs | 8 +- 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 docs/bitbucket-development.md create mode 100644 src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs diff --git a/docs/bitbucket-development.md b/docs/bitbucket-development.md new file mode 100644 index 000000000..e20384260 --- /dev/null +++ b/docs/bitbucket-development.md @@ -0,0 +1,80 @@ +# Bitbucket Authentication, 2FA and OAuth + +By default for authenticating against private Git repositories Bitbucket supports SSH and username/password Basic Auth over HTTPS. +Username/password Basic Auth over HTTPS is also available for REST API access. +Additionally Bitbucket supports App-specific passwords which can be used via Basic Auth as username/app-specific-password. + +To enhance security Bitbucket offers optional Two-Factor Authentication (2FA). When 2FA is enabled username/password Basic Auth access to the REST APIs and to Git repositories is suspended. +At that point users are left with the choice of username/apps-specific-password Basic Auth for REST APIs and Git interactions, OAuth for REST APIs and Git/Hg interactions or SSH for Git/HG interactions and one of the previous choices for REST APIs. +SSH and REST API access are beyond the scope of this document. +Read about [Bitbucket's 2FA implementation](https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html). + +App-specific passwords are not particularly user friendly as once created Bitbucket hides their value, even from the owner. +They are intended for use within application that talk to Bitbucket where application can remember and use the app-specific-password. +[Additional information](https://confluence.atlassian.com/display/BITBUCKET/App+passwords). + +OAuth is the intended authentication method for user interactions with HTTPS remote URL for Git repositories when 2FA is active. +Essentially once a client application has an OAuth access token it can be used in place of a user's password. +Read more about information [Bitbucket's OAuth implementation](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html). + +Bitbucket's OAuth implementation follows the standard specifications for OAuth 2.0, which is out of scope for this document. +However it implements a comparatively rare part of OAuth 2.0 Refresh Tokens. +Bitbucket's Access Token's expire after 1 hour if not revoked, as opposed to GitHub's that expire after 1 year. +When GitHub's Access Tokens expire the user must anticipate in the standard OAuth authentication flow to get a new Access Token. Since this occurs, in theory, once per year this is not too onerous. +Since Bitbucket's Access Tokens expire every hour it is too much to expect a user to go through the OAuth authentication flow every hour. +Bitbucket implements refresh Tokens. +Refresh Tokens are issued to the client application at the same time as Access Tokens. +They can only be used to request a new Access Token, and then only if they have not been revoked. +As such the support for Bitbucket and the use of its OAuth in the Git Credentials Manager differs significantly from how VSTS and GitHub are implemented. +This is explained in more detail below. + +## Multiple User Accounts + +Unlike the GitHub implementation within the Git Credential Manager, the Bitbucket implementation stores 'secrets', passwords, app-specific passwords, or OAuth tokens, with usernames in the [Windows Credential Manager](https://msdn.microsoft.com/en-us/library/windows/desktop/aa374792(v=vs.85).aspx) vault. + +Depending on the circumstances this means either saving an explicit username in to the Windows Credential Manager/Vault or including the username in the URL used as the identifying key of entries in the Windows Credential Manager vault, i.e. using a key such as `git:https://mminns@bitbucket.org/` rather than `git:https://bitbucket.org`. +This means that the Bitbucket implementation in the GCM can support multiple accounts, and usernames, for a single user against Bitbucket, e.g. a personal account and a work account. + +## Authentication User Experience + +When the GCM is triggered by Git, the GCM will check the `host` parameter passed to it. +If it contains `bitbucket.org` it will trigger the Bitbucket related processes. + +### Basic Authentication + +If the GCM needs to prompt the user for credentials they will always be shown an initial dialog where they can enter a username and password. If the `username` parameter was passed into the GCM it is used to pre-populate the username field, although it can be overridden. +When username and password credentials are submitted the GCM will use them to attempt to retrieve a token, for Basic Authentication this token is in effect the password the user just entered. +The GCM retrieves this `token` by checking the password can be used to successfully retrieve the User profile via the Bitbucket REST API. + +If the username and password credentials sent as Basic Authentication credentials works, then the password is identified as the token. The credentials, the username and the password/token, are then stored and the values returned to Git. + +If the request for the User profile via the REST API fails with a 401 return code it indicates the username/password combination is invalid, nothing is stored and nothing is returned to Git. + +However if the request fails with a 403 (Forbidden) return code, this indicates that the username and password are valid but 2FA is enabled on the Bitbucket Account. +When this occurs the user it prompted to complete the OAuth authentication process. + +### OAuth + +OAuth authentication prompts the User with a new dialog where they can trigger OAuth authentication. +This involves opening a browser request to `_https://bitbucket.org/site/oauth2/authorize?response_type=code&client_id={consumerkey}&state=authenticated&scope={scopes}&redirect_uri=http://localhost:34106/_`. +This will trigger a flow on Bitbucket where the user must login, potentially including a 2FA prompt, and authorize the GCM to access Bitbucket with the specified scopes. +The GCM will spawn a temporary, local webserver, listening on port 34106, to handle the OAuth redirect/callback. +Assuming the user successfully logins into Bitbucket and authorizes the GCM this callback will include the Access and Refresh Tokens. + +The Access and Refresh Tokens will be stored against the username and the username/Access Token credentials returned to Git. + +# On-Premise Bitbucket + +On-premise Bitbucket, more correctly known as Bitbucket Server or Bitbucket DC, has a number of differences compared to the cloud instance of Bitbucket, 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 + +See 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 diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs new file mode 100644 index 000000000..57537e35d --- /dev/null +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -0,0 +1,60 @@ +using Microsoft.Git.CredentialManager; +using Microsoft.Git.CredentialManager.Tests.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Atlassian.Bitbucket.Tests +{ + public class BitbucketHostProviderTest + { + [Theory] + // We report that we support unencrypted HTTP here so that we can fail and + // show a helpful error message in the call to `GenerateCredentialAsync` instead. + [InlineData("http", "bitbucket.org", true)] + [InlineData("ssh", "bitbucket.org", false)] + [InlineData("https", "bitbucket.org", true)] + [InlineData("https", "api.bitbucket.org", true)] // Currently does support sub domains. + + [InlineData("https", "bitbucket.ogg", false)] // No support of phony similar tld. + [InlineData("https", "bitbucket.com", false)] // No support of wrong tld. + [InlineData("https", "example.com", false)] // No support of non bitbucket domains. + + [InlineData("http", "bitbucket.my-company-server.com", false)] // Currently no support for named on-premise instances + [InlineData("https", "my-company-server.com", false)] + [InlineData("https", "bitbucket.my.company.server.com", false)] + [InlineData("https", "api.bitbucket.my-company-server.com", false)] + [InlineData("https", "BITBUCKET.My-Company-Server.Com", false)] + public void BitbucketHostProvider_IsSupported(string protocol, string host, bool expected) + { + var input = new InputArguments(new Dictionary + { + ["protocol"] = protocol, + ["host"] = host, + }); + + var provider = new BitbucketHostProvider(new TestCommandContext()); + Assert.Equal(expected, provider.IsSupported(input)); + } + + [Theory] + [InlineData("X-AREQUESTID", "123456789", true)] // only the specific header is acceptable + [InlineData("X-REQUESTID", "123456789", false)] + [InlineData(null, null, false)] + public void BitbucketHostProvider_IsSupported_HttpResponseMessage(string header, string value, bool expected) + { + var input = new HttpResponseMessage(); + if (header != null) + { + input.Headers.Add(header, value); + } + + var provider = new BitbucketHostProvider(new TestCommandContext()); + Assert.Equal(expected, provider.IsSupported(input)); + } + } +} diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs index c6da9002d..8e0b1f102 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -66,8 +66,12 @@ public bool IsSupported(HttpResponseMessage response) return false; } - // TODO: identify Bitbucket on-prem instances from the HTTP response - return false; + // Identify Bitbucket on-prem instances from the HTTP response using the Atlassian specific header X-AREQUESTID + var supported = response.Headers.Contains("X-AREQUESTID"); + + _context.Trace.WriteLine($"Host is{(supported ? null : "n't")} supported as Bitbucket"); + + return supported; } public async Task GetCredentialAsync(InputArguments input) From 65f753c8dab8cbf8f2edac1bdcb012e61d88c21f Mon Sep 17 00:00:00 2001 From: Matt Cooper Date: Wed, 23 Jun 2021 14:54:53 -0400 Subject: [PATCH 05/19] a small "how to use" tweak --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64cefbf31..1e54b0a86 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,11 @@ To uninstall, open Control Panel and navigate to the Programs and Features scree ## How to use -Git Credential Manager Core is called implicitly by Git, when so configured. It is not intended to be called directly by the user. -For example, when pushing (`git push`) to [Azure DevOps](https://dev.azure.com), a window is automatically opened and an OAuth2 flow is started to get your personal access token. +Once it's installed and configured, Git Credential Manager Core is called implicitly by Git. +You don't have to do anything special, and GCM Core isn't intended to be called directly by the user. +For example, when pushing (`git push`) to [Azure DevOps](https://dev.azure.com), [Bitbucket](https://bitbucket.org), or [GitHub](https://github.com), a window will automatically open and walk you through the sign-in process. +(This process will look slightly different for each Git host, and even in some cases, whether you've connected to an on-premises or cloud-hosted Git host.) +Later Git commands in the same repository will re-use existing credentials or tokens that GCM Core has stored for as long as they're valid. Read full command line usage [here](docs/usage.md). From 036e75768c9303e5aea3cd2d8126ea45d51ecab9 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Mon, 21 Jun 2021 11:24:14 -0700 Subject: [PATCH 06/19] Consolidate apt-get release flow This change replaces Python scripts we were calling from the `release-apt-get` pipeline with inline scripts. It also adds support for releasing to Ubuntu repositories hosted at http://packages.microsoft.com/ (hosting location for Microsoft's official apt/yum repos). This allows users to install via apt-get on Hirsute/Bionic. Details to configure appropriate repos can be found here: https://docs.microsoft.com/en-us/windows-server/administration/Linux-Package-Repository-for-Microsoft-Software). --- .github/configure_repoclient.py | 35 ------------- .github/fetch_release.py | 49 ------------------ .github/workflows/release-apt-get.yml | 73 +++++++++++++++++++++------ 3 files changed, 58 insertions(+), 99 deletions(-) delete mode 100644 .github/configure_repoclient.py delete mode 100644 .github/fetch_release.py diff --git a/.github/configure_repoclient.py b/.github/configure_repoclient.py deleted file mode 100644 index 1f02b184f..000000000 --- a/.github/configure_repoclient.py +++ /dev/null @@ -1,35 +0,0 @@ -from os import environ as env -import json - -def check_var(name:str) -> bool: - if name not in env: - print(f"Required env var {name} is missing!") - exit(1) - -for var in ['APT_REPO_ID', 'AZURE_AAD_ID', 'AAD_CLIENT_SECRET']: - check_var(var) - -repo_id = env['APT_REPO_ID'] -aad_id = env['AZURE_AAD_ID'] -password = env['AAD_CLIENT_SECRET'] - -repo_config = { - "AADResource": "https://microsoft.onmicrosoft.com/945999e9-da09-4b5b-878f-b66c414602c0", - "AADTenant": "72f988bf-86f1-41af-91ab-2d7cd011db47", - "AADAuthorityUrl": "https://login.microsoftonline.com", - "server": "azure-apt-cat.cloudapp.net", - "port": "443", - - "repositoryId": repo_id, - "AADClientId": aad_id, - "AADClientSecret": password, -} - -configs = [ - ("config.json", repo_config), -] - -for filename, data in configs: - with open(filename, 'w') as fp: - json.dump(data, fp) - diff --git a/.github/fetch_release.py b/.github/fetch_release.py deleted file mode 100644 index 831af083a..000000000 --- a/.github/fetch_release.py +++ /dev/null @@ -1,49 +0,0 @@ -from os import environ as env -import requests -import time -import json - -if 'RELEASE' in env and env['RELEASE']: - release = env['RELEASE'].strip() -else: - release = "latest" - -release_arg = "latest" if release == "latest" else f"tags/{release}" - -print(f"release_arg set to {release_arg}") - -release_url = f"https://api.github.com/repos/microsoft/git-credential-manager-core/releases/{release_arg}" - -try: - print(f"Fetching release {release_url}") - r = requests.get(release_url) - - # if at first you don't succeed... - if r.status_code != 200: - print(f"Trying again, initial status {r.status_code}") - time.sleep(2) - r = requests.get(release_url) - - print("Response:") - print(json.dumps(r.json(), indent=4)) - - if r.status_code != 200: - raise ValueError(f"Failed to fetch release info from {release_url}") - - asset = [a for a in r.json()['assets'] if a['name'].endswith(".deb")][0] - asset_url = asset['browser_download_url'] - asset_name = asset['name'] - - print(f"Found asset {asset_name}") - print(f"Writing asset URL: {asset_url} to asset_url.txt") - - with open('asset_name.txt', 'w') as f: - f.write(asset_name) - - with open('asset_url.txt', 'w') as f: - f.write(asset_url) - -except RuntimeError as ex: - print("Oh dear...") - print(ex) - exit(1) diff --git a/.github/workflows/release-apt-get.yml b/.github/workflows/release-apt-get.yml index 0096b7496..b888b03d0 100644 --- a/.github/workflows/release-apt-get.yml +++ b/.github/workflows/release-apt-get.yml @@ -14,39 +14,82 @@ jobs: release: runs-on: ubuntu-latest steps: - - name: setup python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - uses: actions/checkout@v2 - uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: 'Download Repo Client' + - name: "Download Repo Client" env: AZ_SUB: ${{ secrets.AZURE_SUBSCRIPTION }} run: | az storage blob download --subscription "$AZ_SUB" --account-name gitcitoolstore -c tools -n azure-repoapi-client_2.0.1_amd64.deb -f repoclient.deb --auth-mode login - name: "Install Repo Client" - env: - APT_REPO_ID: ${{ secrets.APT_REPO_ID }} - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - AAD_CLIENT_SECRET: ${{ secrets.AAD_CLIENT_SECRET }} run: | sudo apt-get install python3-adal --yes sudo dpkg -i repoclient.deb - python .github/configure_repoclient.py rm repoclient.deb + - name: "Configure Repo Client" + uses: actions/github-script@v3 + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + AAD_CLIENT_SECRET: ${{ secrets.AAD_CLIENT_SECRET }} + with: + script: | + for (const key of ['AZURE_AAD_ID', 'AAD_CLIENT_SECRET']) { + if (!process.env[key]) throw new Error(`Required env var ${key} is missing!`) + } + const config = { + AADResource: 'https://microsoft.onmicrosoft.com/945999e9-da09-4b5b-878f-b66c414602c0', + AADTenant: '72f988bf-86f1-41af-91ab-2d7cd011db47', + AADAuthorityUrl: 'https://login.microsoftonline.com', + server: 'azure-apt-cat.cloudapp.net', + port: '443', + AADClientId: process.env.AZURE_AAD_ID, + AADClientSecret: process.env.AAD_CLIENT_SECRET, + repositoryId: '' + } + const fs = require('fs') + fs.writeFileSync('config.json', JSON.stringify(config, null, 2)) + + - name: "Get Release Asset" + id: get-asset + env: + RELEASE: ${{ github.event.inputs.release }} + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const { data } = await github.repos.getRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: process.env.RELEASE || 'latest' + }) + const assets = data.assets.filter(asset => asset.name.endsWith('.deb')) + if (assets.length !== 1) { + throw new Error(`Unexpected number of .deb assets: ${assets.length}`) + } + const fs = require('fs') + const buffer = await github.repos.getReleaseAsset({ + headers: { + accept: 'application/octet-stream' + }, + owner: context.repo.owner, + repo: context.repo.repo, + asset_id: assets[0].id + }) + console.log(buffer) + fs.writeFileSync(assets[0].name, Buffer.from(buffer.data)) + core.setOutput('name', assets[0].name) + - name: "Publish to apt feed" env: RELEASE: ${{ github.event.inputs.release }} run: | - pip install requests - python .github/fetch_release.py - wget "$(cat asset_url.txt)" - repoclient -v v3 -c config.json package add --check --wait 300 "$(cat asset_name.txt)" + for id in ${{ secrets.BIONIC_REPO_ID }} ${{ secrets.HIRSUTE_REPO_ID }} + do + repoclient -v v3 -c config.json package add --check --wait 300 "${{steps.get-asset.outputs.name}}" -r $id + done From 8d778a70cd509fed014ca0307d502c0c6081fadc Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Wed, 30 Jun 2021 21:16:36 -0700 Subject: [PATCH 07/19] Add instructions for `apt-get` install to `README` Updating README.md with instructions for apt-get setup and install for Ubuntu Bionic and Hirsute. --- README.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1e54b0a86..af7f5b08a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,30 @@ sudo /usr/local/share/gcm-core/uninstall.sh ### Linux Debian package (.deb) +`apt-get` support is available for Ubuntu Bionic Beaver (18.04) and Hirsute +Hippo (21.04). Take the following steps to set up and install based on the +version you are running: + +#### Ubuntu 18.04 (Bionic) + +```shell +curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - +sudo apt-add-repository https://packages.microsoft.com/ubuntu/18.04/prod +sudo apt-get update +sudo apt-get install gcmcore +``` + +#### Ubuntu 21.04 (Hirsute) + +```shell +curl -sSL https://packages.microsoft.com/config/ubuntu/21.04/prod.list | sudo tee /etc/apt/sources.list.d/microsoft-prod.list +curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc +sudo apt-get update +sudo apt-get install gcmcore +``` + +#### Other Ubuntu/Debian distributions + Download the latest [.deb package](https://github.com/microsoft/Git-Credential-Manager-Core/releases/latest), and run the following: ```shell @@ -97,9 +121,7 @@ git-credential-manager-core configure Note that Linux distributions [require additional configuration](https://aka.ms/gcmcore-linuxcredstores) to use GCM Core. ---- - -### Linux tarball (.tar.gz) +#### Other distributions Download the latest [tarball](https://github.com/microsoft/Git-Credential-Manager-Core/releases/latest), and run the following: From a3e9dd5ca7f5737fd2f7fea2030223020b805967 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Thu, 1 Jul 2021 11:18:46 -0700 Subject: [PATCH 08/19] Clarify `workflow_dispatch` input description We are currently using 'Release tag' to describe the required input to our `workflow_dispatch` trigger. This is inaccurate - this field actually requires a 'Release id', which I discovered when testing GCM Core `apt-get` deployments yesterday. Updating so that the description doesn't confuse folks running the workflow for a release that is not 'latest'. --- .github/workflows/release-apt-get.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-apt-get.yml b/.github/workflows/release-apt-get.yml index b888b03d0..ea65955fc 100644 --- a/.github/workflows/release-apt-get.yml +++ b/.github/workflows/release-apt-get.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: inputs: release: - description: 'Release Tag' + description: 'Release Id' required: true default: 'latest' From 218c1b0d520f4417b9d4529246907ab9d0c6ea26 Mon Sep 17 00:00:00 2001 From: Matt Cooper Date: Fri, 2 Jul 2021 13:12:19 -0400 Subject: [PATCH 09/19] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af7f5b08a..92fd9411b 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,11 @@ sudo /usr/local/share/gcm-core/uninstall.sh --- -### Linux Debian package (.deb) + + +### Linux + +#### Debian package (.deb) `apt-get` support is available for Ubuntu Bionic Beaver (18.04) and Hirsute Hippo (21.04). Take the following steps to set up and install based on the From 97ab665079190848906c053a538a576256eb67a5 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 19 Jul 2021 14:54:53 +0100 Subject: [PATCH 10/19] plaintextcredstore: create plaintext store with 700 When creating the plaintext credential store root directory, create it with permissions such that only the owner/user can read/write/execute, and not the group or other users. This only impacts newly created store directories; existing directories do not have their permissions modified. Only applies to POSIX platforms. --- docs/linuxcredstores.md | 4 ++ .../Interop/Posix/Native/Stat.cs | 58 +++++++++++++++++++ .../PlaintextCredentialStore.cs | 33 +++++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/shared/Microsoft.Git.CredentialManager/Interop/Posix/Native/Stat.cs diff --git a/docs/linuxcredstores.md b/docs/linuxcredstores.md index 45a42894d..9a253a066 100644 --- a/docs/linuxcredstores.md +++ b/docs/linuxcredstores.md @@ -134,6 +134,10 @@ the environment variable `GCM_PLAINTEXT_STORE_PATH` environment variable. If the directory does not exist is will be created. +On POSIX platforms the newly created store directory will have permissions set +such that only the owner can `r`ead/`w`rite/e`x`ecute (`700` or `drwx---`). +Permissions on existing directories will not be modified. + ---

diff --git a/src/shared/Microsoft.Git.CredentialManager/Interop/Posix/Native/Stat.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/Posix/Native/Stat.cs new file mode 100644 index 000000000..595b115a7 --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/Posix/Native/Stat.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Git.CredentialManager.Interop.Posix.Native +{ + public static class Stat + { + [DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern int chmod(string path, NativeFileMode mode); + } + [Flags] + public enum NativeFileMode + { + NONE = 0, + + // Default permissions (RW for owner, RW for group, RW for other) + DEFAULT = S_IWOTH | S_IROTH | S_IWGRP | S_IRGRP | S_IWUSR | S_IRUSR, + + // All file access permissions (RWX for owner, group, and other) + ACCESSPERMS = S_IRWXO | S_IRWXU | S_IRWXG, + + // Read for owner (0000400) + S_IRUSR = 0x100, + // Write for owner (0000200) + S_IWUSR = 0x080, + // Execute for owner (0000100) + S_IXUSR = 0x040, + // Access permissions for owner + S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR, + + // Read for group (0000040) + S_IRGRP = 0x020, + // Write for group (0000020) + S_IWGRP = 0x010, + // Execute for group (0000010) + S_IXGRP = 0x008, + // Access permissions for group + S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP, + + // Read for other (0000004) + S_IROTH = 0x004, + // Write for other (0000002) + S_IWOTH = 0x002, + // Execute for other (0000001) + S_IXOTH = 0x001, + // Access permissions for other + S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH, + + // Set user ID on execution (0004000) + S_ISUID = 0x800, + // Set group ID on execution (0002000) + S_ISGID = 0x400, + // Sticky bit (0001000) + S_ISVTX = 0x200, + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/PlaintextCredentialStore.cs b/src/shared/Microsoft.Git.CredentialManager/PlaintextCredentialStore.cs index 954f2df7e..cc280d52c 100644 --- a/src/shared/Microsoft.Git.CredentialManager/PlaintextCredentialStore.cs +++ b/src/shared/Microsoft.Git.CredentialManager/PlaintextCredentialStore.cs @@ -59,6 +59,9 @@ public ICredential Get(string service, string account) public void AddOrUpdate(string service, string account, string secret) { + // Ensure the store root exists and permissions are set + EnsureStoreRoot(); + string serviceSlug = CreateServiceSlug(service); string servicePath = Path.Combine(StoreRoot, serviceSlug); @@ -161,6 +164,36 @@ protected virtual void SerializeCredential(FileCredential credential) } } + ///

+ /// Ensure the store root directory exists. If it does not, create a new directory with + /// permissions that only permit the owner to read/write/execute. Permissions on an existing + /// directory are not modified. + /// + private void EnsureStoreRoot() + { + if (FileSystem.DirectoryExists(StoreRoot)) + { + // Don't touch the permissions on the existing directory + return; + } + + FileSystem.CreateDirectory(StoreRoot); + + // We only set file system permissions on POSIX platforms + if (!PlatformUtils.IsPosix()) + { + return; + } + + // Set store root permissions such that only the owner can read/write/execute + var mode = Interop.Posix.Native.NativeFileMode.S_IRUSR | + Interop.Posix.Native.NativeFileMode.S_IWUSR | + Interop.Posix.Native.NativeFileMode.S_IXUSR; + + // Ignore the return code.. this is a best effort only + Interop.Posix.Native.Stat.chmod(StoreRoot, mode); + } + private string CreateServiceSlug(string service) { var sb = new StringBuilder(); From d0f8121e261e5ffd0fcae1fea47f8a879984909d Mon Sep 17 00:00:00 2001 From: M Hickford Date: Tue, 20 Jul 2021 17:11:36 +0100 Subject: [PATCH 11/19] Update linuxcredstores.md Clarify that is necessary to run `git-credential-manager-core configure`. --- docs/linuxcredstores.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/linuxcredstores.md b/docs/linuxcredstores.md index 45a42894d..d964227b6 100644 --- a/docs/linuxcredstores.md +++ b/docs/linuxcredstores.md @@ -8,7 +8,7 @@ Manager Core (GCM Core) manages on Linux platforms: 3. Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache) 4. Plaintext files -By default, GCM Core comes unconfigured. You can select which credential store +By default, GCM Core comes unconfigured. After running `git-credential-manager-core configure`, you can select which credential store to use by setting the [`GCM_CREDENTIAL_STORE`](environment.md#GCM_CREDENTIAL_STORE) environment variable, or the [`credential.credentialStore`](configuration.md#credentialcredentialstore) Git configuration setting. From f2065964cdbd09c94fec01e85225deaaa7299b3c Mon Sep 17 00:00:00 2001 From: Garvit Joshi Date: Sat, 24 Jul 2021 20:19:55 +0530 Subject: [PATCH 12/19] Updated ISSUE_TEMPLATE from version to --version --- .github/ISSUE_TEMPLATE/auth-problem.md | 2 +- .github/ISSUE_TEMPLATE/experimental.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/auth-problem.md b/.github/ISSUE_TEMPLATE/auth-problem.md index 8973dc52b..316cd8a65 100644 --- a/.github/ISSUE_TEMPLATE/auth-problem.md +++ b/.github/ISSUE_TEMPLATE/auth-problem.md @@ -8,7 +8,7 @@ assignees: '' **Which version of GCM Core are you using?** -From a terminal, run `git-credential-manager-core version` and paste the output. +From a terminal, run `git-credential-manager-core --version` and paste the output.