/// <remarks> /// For dev.azure.com-style URLs we use the path arg to get the Azure DevOps organization name. /// We ensure the presence of the path arg by setting credential.useHttpPath = true at install time. /// /// The result of this workaround is that we are now unable to determine if the user wanted to store /// credentials with the full path or not for dev.azure.com-style URLs. /// /// Rather than always assume we're storing credentials against the full path, and therefore resulting /// in an personal access token being created per remote URL/repository, we never store against /// the full path and always store with the organization URL "dev.azure.com/org". /// /// For visualstudio.com-style URLs we know the AzDevOps organization name from the host arg, and /// don't set the useHttpPath option. This means if we get the full path for a vs.com-style URL /// we can store against the full remote path (the intended design). /// /// Users that need to clone a repository from Azure Repos against the full path therefore must /// use the vs.com-style remote URL and not the dev.azure.com one. /// </remarks> private static string GetServiceName(InputArguments input) { if (!input.TryGetHostAndPort(out string hostName, out _)) { throw new InvalidOperationException("Failed to parse host name and/or port"); } // dev.azure.com if (UriHelpers.IsDevAzureComHost(hostName)) { // We can never store the new dev.azure.com-style URLs against the full path because // we have forced the useHttpPath option to true to in order to retrieve the AzDevOps // organization name from Git. return(UriHelpers.CreateOrganizationUri(input).AbsoluteUri.TrimEnd('/')); } // *.visualstudio.com if (UriHelpers.IsVisualStudioComHost(hostName)) { // If we're given the full path for an older *.visualstudio.com-style URL then we should // respect that in the service name. return(input.GetRemoteUri().AbsoluteUri.TrimEnd('/')); } throw new InvalidOperationException("Host is not Azure DevOps."); }
private void VerifyOAuthFlowRan(string password, bool storedAccount, bool expected, InputArguments input, Task <ICredential> credential, string preconfiguredAuthModes) { 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 <AuthenticationModes>()), 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); } }
public override async Task <ICredential> GenerateCredentialAsync(InputArguments input) { ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Exception("Unencrypted HTTP is not supported for GitLab. Ensure the repository remote URL is using HTTPS."); } Uri remoteUri = input.GetRemoteUri(); AuthenticationModes authModes = GetSupportedAuthenticationModes(remoteUri); AuthenticationPromptResult promptResult = await _gitLabAuth.GetAuthenticationAsync(remoteUri, input.UserName, authModes); switch (promptResult.AuthenticationMode) { case AuthenticationModes.Basic: case AuthenticationModes.Pat: return(promptResult.Credential); case AuthenticationModes.Browser: return(await GenerateOAuthCredentialAsync(input)); default: throw new ArgumentOutOfRangeException(nameof(promptResult)); } }
private void VerifyBasicAuthFlowRan(string password, bool storedAccount, bool expected, InputArguments input, System.Threading.Tasks.Task <ICredential> credential, string preconfiguredAuthModes) { Assert.Equal(expected, credential != null); var remoteUri = input.GetRemoteUri(); if (storedAccount) { // stored username/password so no need to prompt the user for them bitbucketAuthentication.Verify(m => m.GetBasicCredentialsAsync(remoteUri, input.UserName), Times.Never); } else { // no username/password credentials so prompt the user for them bitbucketAuthentication.Verify(m => m.GetBasicCredentialsAsync(remoteUri, input.UserName), Times.Once); } // check username/password for Bitbucket.org if ((preconfiguredAuthModes == null && "bitbucket.org" == remoteUri.Host) || (preconfiguredAuthModes != null && preconfiguredAuthModes.Contains("oauth"))) { bitbucketApi.Verify(m => m.GetUserInformationAsync(input.UserName, password, false), Times.Once); } }
private void VerifyInteractiveOAuthFlowRan(string password, InputArguments input, System.Threading.Tasks.Task <ICredential> credential) { var remoteUri = input.GetRemoteUri(); // Basic Auth 403-ed so push user through OAuth flow bitbucketAuthentication.Verify(m => m.ShowOAuthRequiredPromptAsync(), Times.Once); }
private static void MockUserEntersValidBasicCredentials(Mock <IBitbucketAuthentication> bitbucketAuthentication, InputArguments input, string password) { var remoteUri = input.GetRemoteUri(); bitbucketAuthentication.Setup(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny <AuthenticationModes>())) .ReturnsAsync(new CredentialsPromptResult(AuthenticationModes.Basic, new TestCredential(input.Host, input.UserName, password))); }
private static void MockStoredAccount(TestCommandContext context, InputArguments input, string password) { var remoteUri = input.GetRemoteUri(); var remoteUrl = remoteUri.AbsoluteUri.Substring(0, remoteUri.AbsoluteUri.Length - 1); context.CredentialStore.Add(remoteUrl, new TestCredential(input.Host, input.UserName, password)); }
public Task StoreCredentialAsync(InputArguments input) { Uri remoteUri = input.GetRemoteUri(); if (UsePersonalAccessTokens()) { string service = GetServiceName(remoteUri); // We always store credentials against the given username argument for // both vs.com and dev.azure.com-style URLs. string account = input.UserName; // Add or update the credential in the store. _context.Trace.WriteLine($"Storing credential with service={service} account={account}..."); _context.CredentialStore.AddOrUpdate(service, account, input.Password); _context.Trace.WriteLine("Credential was successfully stored."); } else { string orgName = UriHelpers.GetOrganizationName(remoteUri); _context.Trace.WriteLine($"Signing user {input.UserName} in to organization '{orgName}'..."); _bindingManager.SignIn(orgName, input.UserName); } return(Task.CompletedTask); }
public Task EraseCredentialAsync(InputArguments input) { Uri remoteUri = input.GetRemoteUri(); if (UsePersonalAccessTokens()) { string service = GetServiceName(remoteUri); string account = GetAccountNameForCredentialQuery(input); // Try to locate an existing credential _context.Trace.WriteLine($"Erasing stored credential in store with service={service} account={account}..."); if (_context.CredentialStore.Remove(service, account)) { _context.Trace.WriteLine("Credential was successfully erased."); } else { _context.Trace.WriteLine("No credential was erased."); } } else { string orgName = UriHelpers.GetOrganizationName(remoteUri); _context.Trace.WriteLine($"Signing out of organization '{orgName}'..."); _bindingManager.SignOut(orgName); // Clear the authority cache in case this was the reason for failure _authorityCache.EraseAuthority(orgName); } return(Task.CompletedTask); }
internal async Task ExecuteAsync() { Context.Trace.WriteLine($"Start '{Name}' command..."); // Parse standard input arguments // git-credential treats the keys as case-sensitive; so should we. IDictionary <string, string> inputDict = await Context.Streams.In.ReadDictionaryAsync(StringComparer.Ordinal); var input = new InputArguments(inputDict); // Validate minimum arguments are present EnsureMinimumInputArguments(input); // Set the remote URI to scope settings to throughout the process from now on Context.Settings.RemoteUri = input.GetRemoteUri(); // Determine the host provider Context.Trace.WriteLine("Detecting host provider for input:"); Context.Trace.WriteDictionarySecrets(inputDict, new [] { "password" }, StringComparer.OrdinalIgnoreCase); IHostProvider provider = await _hostProviderRegistry.GetProviderAsync(input); Context.Trace.WriteLine($"Host provider '{provider.Name}' was selected."); await ExecuteInternalAsync(input, provider); Context.Trace.WriteLine($"End '{Name}' command..."); }
public override async Task <ICredential> GenerateCredentialAsync(InputArguments input) { ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Exception("Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); } Uri remoteUri = input.GetRemoteUri(); string service = GetServiceName(input); AuthenticationModes authModes = await GetSupportedAuthenticationModesAsync(remoteUri); AuthenticationPromptResult promptResult = await _gitHubAuth.GetAuthenticationAsync(remoteUri, input.UserName, authModes); switch (promptResult.AuthenticationMode) { case AuthenticationModes.Basic: GitCredential patCredential = await GeneratePersonalAccessTokenAsync(remoteUri, promptResult.Credential); // HACK: Store the PAT immediately in case this PAT is not valid for SSO. // We don't know if this PAT is valid for SAML SSO and if it's not Git will fail // with a 403 and call neither 'store' or 'erase'. The user is expected to fiddle with // the PAT permissions manually on the web and then retry the Git operation. // We must store the PAT now so they can resume/repeat the operation with the same, // now SSO authorized, PAT. // See: https://github.com/GitCredentialManager/git-credential-manager/issues/133 Context.CredentialStore.AddOrUpdate(service, patCredential.Account, patCredential.Password); return(patCredential); case AuthenticationModes.Browser: return(await GenerateOAuthCredentialAsync(remoteUri, useBrowser : true)); case AuthenticationModes.Device: return(await GenerateOAuthCredentialAsync(remoteUri, useBrowser : false)); case AuthenticationModes.Pat: // The token returned by the user should be good to use directly as the password for Git string token = promptResult.Credential.Password; // Resolve the GitHub user handle if we don't have a specific username already from the // initial request. The reason for this is GitHub requires a (any?) value for the username // when Git makes calls to GitHub. string userName = promptResult.Credential.Account; if (userName is null) { GitHubUserInfo userInfo = await _gitHubApi.GetUserInfoAsync(remoteUri, token); userName = userInfo.Login; } return(new GitCredential(userName, token)); default: throw new ArgumentOutOfRangeException(nameof(promptResult)); } }
public void InputArguments_GetRemoteUri_NoAuthority_ReturnsNull() { var dict = new Dictionary <string, string>(); var inputArgs = new InputArguments(dict); Uri actualUri = inputArgs.GetRemoteUri(); Assert.Null(actualUri); }
private void VerifyInteractiveOAuthFlowNeverRan(InputArguments input, System.Threading.Tasks.Task <ICredential> credential) { 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 <string>()), Times.Never); // never check access token works bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never); }
private static string GetRefreshTokenServiceName(InputArguments input) { Uri baseUri = input.GetRemoteUri(includeUser: false); // The refresh token key never includes the path component. // Instead we use the path component to specify this is the "refresh_token". Uri uri = new UriBuilder(baseUri) { Path = "/refresh_token" }.Uri; return(uri.AbsoluteUri.TrimEnd('/')); }
private void VerifyInteractiveBasicAuthFlowRan(string password, InputArguments input, Task <ICredential> credential) { var remoteUri = input.GetRemoteUri(); // verify users was prompted for username/password credentials bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny <AuthenticationModes>()), 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); } }
public Task StoreCredentialAsync(InputArguments input) { // It doesn't matter if this is an OAuth access token, or the literal username & password // because we store them the same way, against the same credential key in the store. // The OAuth refresh token is already stored on the 'get' request. Uri remoteUri = input.GetRemoteUri(); string service = GetServiceName(remoteUri); _context.Trace.WriteLine("Storing credential..."); _context.CredentialStore.AddOrUpdate(service, input.UserName, input.Password); _context.Trace.WriteLine("Credential was successfully stored."); return(Task.CompletedTask); }
public override async Task <ICredential> GenerateCredentialAsync(InputArguments input) { ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Exception("Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); } Uri orgUri = UriHelpers.CreateOrganizationUri(input); Uri remoteUri = input.GetRemoteUri(); // Determine the MS authentication authority for this organization Context.Trace.WriteLine("Determining Microsoft Authentication Authority..."); string authAuthority = await _azDevOps.GetAuthorityAsync(orgUri); Context.Trace.WriteLine($"Authority is '{authAuthority}'."); // Get an AAD access token for the Azure DevOps SPS Context.Trace.WriteLine("Getting Azure AD access token..."); JsonWebToken accessToken = await _msAuth.GetAccessTokenAsync( authAuthority, AzureDevOpsConstants.AadClientId, AzureDevOpsConstants.AadRedirectUri, AzureDevOpsConstants.AadResourceId, remoteUri, null); string atUser = accessToken.GetAzureUserName(); Context.Trace.WriteLineSecrets($"Acquired Azure access token. User='******' Token='{{0}}'", new object[] { accessToken.EncodedToken }); // Ask the Azure DevOps instance to create a new PAT var patScopes = new[] { AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite, AzureDevOpsConstants.PersonalAccessTokenScopes.ArtifactsRead }; Context.Trace.WriteLine($"Creating Azure DevOps PAT with scopes '{string.Join(", ", patScopes)}'..."); string pat = await _azDevOps.CreatePersonalAccessTokenAsync( orgUri, accessToken, patScopes); Context.Trace.WriteLineSecrets("PAT created. PAT='{0}'", new object[] { pat }); return(new GitCredential(Constants.PersonalAccessTokenUserName, pat)); }
private void VerifyOAuthFlowDidNotRun(string password, bool expected, InputArguments input, System.Threading.Tasks.Task <ICredential> credential) { 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 <string>()), Times.Never); // never check access token works bitbucketApi.Verify(m => m.GetUserInformationAsync(null, MOCK_ACCESS_TOKEN, true), Times.Never); }
private void VerifyBasicAuthFlowRan(string password, bool expected, InputArguments input, Task <ICredential> credential, string preconfiguredAuthModes) { Assert.Equal(expected, credential != null); var remoteUri = input.GetRemoteUri(); bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny <AuthenticationModes>()), 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); } }
public async Task <ICredential> GetCredentialAsync(InputArguments input) { // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") && input.TryGetHostAndPort(out string host, out _) && IsBitbucketOrg(host)) { throw new Exception("Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS."); } Uri remoteUri = input.GetRemoteUri(); AuthenticationModes authModes = GetSupportedAuthenticationModes(remoteUri); return(await GetStoredCredentials(remoteUri, input.UserName, authModes) ?? await GetRefreshedCredentials(remoteUri, input.UserName, authModes)); }
public void InputArguments_GetRemoteUri_Authority_ReturnsUriWithAuthority() { var expectedUri = new Uri("https://example.com/"); var dict = new Dictionary <string, string> { ["protocol"] = "https", ["host"] = "example.com" }; var inputArgs = new InputArguments(dict); Uri actualUri = inputArgs.GetRemoteUri(); Assert.NotNull(actualUri); Assert.Equal(expectedUri, actualUri); }
// <remarks>Stores OAuth tokens as a side effect</remarks> public override async Task <ICredential> GetCredentialAsync(InputArguments input) { string service = GetServiceName(input); ICredential credential = Context.CredentialStore.Get(service, input.UserName); if (credential?.Account == "oauth2" && await IsOAuthTokenExpired(input.GetRemoteUri(), credential.Password)) { Context.Trace.WriteLine("Removing expired OAuth access token..."); Context.CredentialStore.Remove(service, credential.Account); credential = null; } if (credential != null) { return(credential); } string refreshService = GetRefreshTokenServiceName(input); string refreshToken = Context.CredentialStore.Get(refreshService, input.UserName)?.Password; if (refreshToken != null) { Context.Trace.WriteLine("Refreshing OAuth token..."); try { credential = await RefreshOAuthCredentialAsync(input, refreshToken); } catch (Exception e) { Context.Terminal.WriteLine($"OAuth token refresh failed: {e.Message}"); } } credential ??= await GenerateCredentialAsync(input); if (credential is OAuthCredential oAuthCredential) { Context.Trace.WriteLine("Pre-emptively storing OAuth access and refresh tokens..."); // freshly-generated OAuth credential // store credential, since we know it to be valid (whereas Git will only store credential if git push succeeds) Context.CredentialStore.AddOrUpdate(service, oAuthCredential.Account, oAuthCredential.AccessToken); // store refresh token under a separate service Context.CredentialStore.AddOrUpdate(refreshService, oAuthCredential.Account, oAuthCredential.RefreshToken); } return(credential); }
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 <AuthenticationModes>()), Times.Once); } else { // never prompt the user for basic credentials bitbucketAuthentication.Verify(m => m.GetCredentialsAsync(remoteUri, input.UserName, It.IsAny <AuthenticationModes>()), Times.Never); } }
private void VerifyBasicAuthFlowDidNotRun(string password, bool expected, InputArguments input, bool storedAccount, System.Threading.Tasks.Task <ICredential> credential, string preconfiguredAuthModes) { Assert.Equal(expected, credential != null); var remoteUri = input.GetRemoteUri(); if (!storedAccount && (preconfiguredAuthModes == null || preconfiguredAuthModes.Contains("basic"))) { // never prompt the user for basic credentials bitbucketAuthentication.Verify(m => m.GetBasicCredentialsAsync(remoteUri, input.UserName), Times.Once); } else { // never prompt the user for basic credentials bitbucketAuthentication.Verify(m => m.GetBasicCredentialsAsync(remoteUri, input.UserName), Times.Never); } }
public Task EraseCredentialAsync(InputArguments input) { // Erase the stored credential (which may be either the literal username & password, or // the OAuth access token). We don't need to erase the OAuth refresh token because on the // next 'get' request, if the RT is bad we will erase and reacquire a new one at that point. Uri remoteUri = input.GetRemoteUri(); string service = GetServiceName(remoteUri); _context.Trace.WriteLine("Erasing credential..."); if (_context.CredentialStore.Remove(service, input.UserName)) { _context.Trace.WriteLine("Credential was successfully erased."); } else { _context.Trace.WriteLine("Credential was not erased."); } return(Task.CompletedTask); }
public void InputArguments_GetRemoteUri_IncludeUser_Authority_ReturnsUriWithAuthorityAndUser() { var expectedUri = new Uri("https://[email protected]/"); var dict = new Dictionary <string, string> { ["protocol"] = "https", ["host"] = "example.com", // Username should appear in the returned URI; the password should not ["username"] = "******", ["password"] = "******" }; var inputArgs = new InputArguments(dict); Uri actualUri = inputArgs.GetRemoteUri(includeUser: true); Assert.NotNull(actualUri); Assert.Equal(expectedUri, actualUri); }
public override async Task <ICredential> GenerateCredentialAsync(InputArguments input) { ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Exception("Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); } Uri remoteUri = input.GetRemoteUri(); string service = GetServiceName(input); AuthenticationModes authModes = await GetSupportedAuthenticationModesAsync(remoteUri); AuthenticationPromptResult promptResult = await _gitHubAuth.GetAuthenticationAsync(remoteUri, input.UserName, authModes); switch (promptResult.AuthenticationMode) { case AuthenticationModes.Basic: GitCredential patCredential = await GeneratePersonalAccessTokenAsync(remoteUri, promptResult.BasicCredential); // HACK: Store the PAT immediately in case this PAT is not valid for SSO. // We don't know if this PAT is valid for SAML SSO and if it's not Git will fail // with a 403 and call neither 'store' or 'erase'. The user is expected to fiddle with // the PAT permissions manually on the web and then retry the Git operation. // We must store the PAT now so they can resume/repeat the operation with the same, // now SSO authorized, PAT. // See: https://github.com/microsoft/Git-Credential-Manager-Core/issues/133 Context.CredentialStore.AddOrUpdate(service, patCredential.Account, patCredential.Password); return(patCredential); case AuthenticationModes.OAuth: return(await GenerateOAuthCredentialAsync(remoteUri)); default: throw new ArgumentOutOfRangeException(nameof(promptResult)); } }
public void InputArguments_GetRemoteUri_AuthorityPathUserInfo_ReturnsUriWithAuthorityAndPath() { var expectedUri = new Uri("https://example.com/an/example/path"); var dict = new Dictionary <string, string> { ["protocol"] = "https", ["host"] = "example.com", ["path"] = "an/example/path", // Username and password are not expected to appear in the returned URI ["username"] = "******", ["password"] = "******" }; var inputArgs = new InputArguments(dict); Uri actualUri = inputArgs.GetRemoteUri(); Assert.NotNull(actualUri); Assert.Equal(expectedUri, actualUri); }
public async Task <ICredential> GetCredentialAsync(InputArguments input) { Uri remoteUri = input.GetRemoteUri(); if (UsePersonalAccessTokens()) { string service = GetServiceName(remoteUri); string account = GetAccountNameForCredentialQuery(input); _context.Trace.WriteLine($"Looking for existing credential in store with service={service} account={account}..."); ICredential credential = _context.CredentialStore.Get(service, account); if (credential == null) { _context.Trace.WriteLine("No existing credentials found."); // No existing credential was found, create a new one _context.Trace.WriteLine("Creating new credential..."); credential = await GeneratePersonalAccessTokenAsync(input); _context.Trace.WriteLine("Credential created."); } else { _context.Trace.WriteLine("Existing credential found."); } return(credential); } else { // Include the username request here so that we may use it as an override // for user account lookups when getting Azure Access Tokens. var azureResult = await GetAzureAccessTokenAsync(remoteUri, input.UserName); return(new GitCredential(azureResult.AccountUpn, azureResult.AccessToken)); } }
public override bool IsSupported(InputArguments input) { if (input is null) { return(false); } // We do not support unencrypted HTTP communications to GitLab, // but we report `true` here for HTTP so that we can show a helpful // error message for the user in `CreateCredentialAsync`. if (!StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") && !StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https")) { return(false); } if (GitLabConstants.IsGitLabDotCom(input.GetRemoteUri())) { return(true); } // Split port number and hostname from host input argument if (!input.TryGetHostAndPort(out string hostName, out _)) { return(false); } string[] domains = hostName.Split(new char[] { '.' }); // GitLab[.subdomain].domain.tld if (domains.Length >= 3 && StringComparer.OrdinalIgnoreCase.Equals(domains[0], "gitlab")) { return(true); } return(false); }