Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update support for Bitbucket Server #141

Closed
12 changes: 12 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
"console": "integratedTerminal",
"stopAtEntry": false,
},
{
"name": "Git Credential Manager (store)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/out/shared/Git-Credential-Manager/bin/Debug/netcoreapp3.1/git-credential-manager-core.dll",
"args": ["store"],
"cwd": "${workspaceFolder}/out/shared/Git-Credential-Manager",
"console": "integratedTerminal",
"stopAtEntry": false,
},
{
"name": ".NET Core Attach",
"type": "coreclr",
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ ID|Provider
`auto` _(default)_|_\[automatic\]_
`azure-repos`|Azure Repos
`github`|GitHub
`bitbucket`|Bitbucket
`generic`|Generic (any other provider not listed above)

Automatic provider selection is based on the remote URL.
Expand Down Expand Up @@ -91,6 +92,7 @@ Authority|Provider(s)
`auto` _(default)_|_\[automatic\]_
`msa`, `microsoft`, `microsoftaccount`,<br/>`aad`, `azure`, `azuredirectory`,</br>`live`, `liveconnect`, `liveid`|Azure Repos<br/>_(supports Microsoft Authentication)_
`github`|GitHub<br/>_(supports GitHub Authentication)_
`bitbucket`|Bitbucket.org<br/>_(supports Basic Authentication and OAuth)_<br/>Bitbucket Server<br/>_(supports Basic Authentication)_
`basic`, `integrated`, `windows`, `kerberos`, `ntlm`,<br/>`tfs`, `sso`|Generic<br/>_(supports Basic and Windows Integrated Authentication)_

#### Example
Expand Down
66 changes: 59 additions & 7 deletions src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,16 @@ public bool IsSupported(InputArguments input)

public async Task<ICredential> GetCredentialAsync(InputArguments input)
{
// Compute the target URI
Uri targetUri = GetTargetUri(input);

// We should not allow unencrypted communication and should inform the user
if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http"))
if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")
&& !IsBitbucketServer(targetUri))
{
throw new Exception("Unencrypted HTTP is not supported for Bitbucket. Ensure the repository remote URL is using HTTPS.");
throw new Exception("Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS.");
}

// Compute the target URI
Uri targetUri = GetTargetUri(input);

// Try and get the username specified in the remote URL if any
string targetUriUser = targetUri.GetUserName();

Expand Down Expand Up @@ -93,7 +94,7 @@ public async Task<ICredential> GetCredentialAsync(InputArguments input)
// or we have a freshly captured user/pass. Regardless, we must check if these credentials
// pass and two-factor requirement on the account.
_context.Trace.WriteLine("Checking if two-factor requirements for stored credentials...");
bool requires2Fa = await RequiresTwoFactorAuthenticationAsync(credential);
bool requires2Fa = await RequiresTwoFactorAuthenticationAsync(credential, targetUri);
if (!requires2Fa)
{
_context.Trace.WriteLine("Two-factor requirement passed with stored credentials");
Expand Down Expand Up @@ -183,6 +184,19 @@ public Task StoreCredentialAsync(InputArguments input)
_context.CredentialStore.AddOrUpdate(credentialKey, credential);
_context.Trace.WriteLine("Credential was successfully stored.");

Uri targetUri = GetTargetUri(input);
if (IsBitbucketServer(targetUri))
{
// BBS doesn't usually include the username in the urls which means they aren't included in the GET call,
// which means if we store only with the username the credentials are never found again ...
// This does have the potential to overwrite itself for different BbS accounts,
// but typically BbS doesn't encourage multiple user accounts
string bbsCredentialKey = GetBitbucketServerCredentialKey(input);
_context.Trace.WriteLine($"Storing Bitbucket Server credential with key '{bbsCredentialKey}'...");
_context.CredentialStore.AddOrUpdate(bbsCredentialKey, credential);
_context.Trace.WriteLine("Bitbucket Server Credential was successfully stored.");
}

return Task.CompletedTask;
}

Expand Down Expand Up @@ -220,8 +234,14 @@ private async Task<string> ResolveOAuthUserNameAsync(string accessToken)
throw new Exception($"Failed to resolve username. HTTP: {result.StatusCode}");
}

private async Task<bool> RequiresTwoFactorAuthenticationAsync(ICredential credentials)
private async Task<bool> RequiresTwoFactorAuthenticationAsync(ICredential credentials, Uri targetUri)
{
if (IsBitbucketServer(targetUri))
{
// BBS does not support 2FA out of the box so neither does GCM
return false;
}

RestApiResult<UserInfo> result = await _bitbucketApi.GetUserInformationAsync(credentials.UserName, credentials.Password, false);
switch (result.StatusCode)
{
Expand Down Expand Up @@ -257,6 +277,21 @@ private string GetCredentialKey(InputArguments input)
return $"git:{url}";
}

private string GetBitbucketServerCredentialKey(InputArguments input)
{
// The credential (user/pass or an OAuth access token) key is the full target URI.
// If the full path is included (credential.useHttpPath = true) then respect that.
string url = GetBitbucketServerTargetUri(input).AbsoluteUri;

// Trim trailing slash
if (url.EndsWith("/"))
{
url = url.Substring(0, url.Length - 1);
}

return $"git:{url}";
}

private string GetRefreshTokenKey(InputArguments input)
{
Uri targetUri = GetTargetUri(input);
Expand Down Expand Up @@ -297,6 +332,23 @@ private static Uri GetTargetUri(InputArguments input)
return uri;
}

private static Uri GetBitbucketServerTargetUri(InputArguments input)
{
Uri uri = new UriBuilder
{
Scheme = input.Protocol,
Host = input.Host,
Path = input.Path
}.Uri;

return uri;
}

private bool IsBitbucketServer(Uri targetUri)
{
return !targetUri.Host.Equals(BitbucketConstants.BitbucketBaseUrlHost);
}

#endregion

public void Dispose()
Expand Down