diff --git a/AzureDevOps.Authentication/Proxy/AzureDevOps.Authentication.Proxy.csproj b/AzureDevOps.Authentication/Proxy/AzureDevOps.Authentication.Proxy.csproj index a5a137283..602df84d1 100644 --- a/AzureDevOps.Authentication/Proxy/AzureDevOps.Authentication.Proxy.csproj +++ b/AzureDevOps.Authentication/Proxy/AzureDevOps.Authentication.Proxy.csproj @@ -7,7 +7,10 @@ Properties AzureDevOps.Authentication.Test AzureDevOps.Authentication.Proxy - + + + v4.5.2 + @@ -31,8 +34,8 @@ ..\..\packages\Moq.4.8.2\lib\net45\Moq.dll - - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll @@ -56,7 +59,9 @@ - + + Designer + diff --git a/AzureDevOps.Authentication/Proxy/packages.config b/AzureDevOps.Authentication/Proxy/packages.config index 4756c7bb1..bb78bac8d 100644 --- a/AzureDevOps.Authentication/Proxy/packages.config +++ b/AzureDevOps.Authentication/Proxy/packages.config @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/AzureDevOps.Authentication/Src/AzureDevOps.Authentication.csproj b/AzureDevOps.Authentication/Src/AzureDevOps.Authentication.csproj index 7d81070b4..12803fd77 100644 --- a/AzureDevOps.Authentication/Src/AzureDevOps.Authentication.csproj +++ b/AzureDevOps.Authentication/Src/AzureDevOps.Authentication.csproj @@ -10,7 +10,8 @@ {19770407-D7D8-4A37-914C-F552FF4B90D4} AzureDevOps.Authentication AzureDevOps.Authentication - v4.5.1 + v4.5.2 + @@ -18,8 +19,8 @@ $(ProjectDir)$(OutputPath) - - ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.4.5.0\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll diff --git a/Bitbucket.Authentication/Proxy/Bitbucket.Authentication.Proxy.csproj b/Bitbucket.Authentication/Proxy/Bitbucket.Authentication.Proxy.csproj index a5f75d085..ae4e06f44 100644 --- a/Bitbucket.Authentication/Proxy/Bitbucket.Authentication.Proxy.csproj +++ b/Bitbucket.Authentication/Proxy/Bitbucket.Authentication.Proxy.csproj @@ -7,7 +7,10 @@ Properties Bitbucket.Authentication.Test Bitbucket.Authentication.Proxy - + + + v4.5.2 + @@ -31,8 +34,8 @@ ..\..\packages\Moq.4.8.2\lib\net45\Moq.dll - - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll diff --git a/Bitbucket.Authentication/Src/Bitbucket.Authentication.csproj b/Bitbucket.Authentication/Src/Bitbucket.Authentication.csproj index 3f52f585e..bfefa0b41 100644 --- a/Bitbucket.Authentication/Src/Bitbucket.Authentication.csproj +++ b/Bitbucket.Authentication/Src/Bitbucket.Authentication.csproj @@ -10,7 +10,8 @@ {EE663736-5BAD-4CA6-A4F8-99978925AD8A} Bitbucket.Authentication Atlassian.Bitbucket.Authentication - v4.5.1 + v4.5.2 + diff --git a/Bitbucket.Authentication/Test/Bitbucket.Authentication.Test.csproj b/Bitbucket.Authentication/Test/Bitbucket.Authentication.Test.csproj index 53c57081d..06ffc35fa 100644 --- a/Bitbucket.Authentication/Test/Bitbucket.Authentication.Test.csproj +++ b/Bitbucket.Authentication/Test/Bitbucket.Authentication.Test.csproj @@ -14,7 +14,10 @@ $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages False UnitTest - + + + v4.5.2 + diff --git a/Cli/Manager/Cli-Manager.csproj b/Cli/Manager/Cli-Manager.csproj index 944e771d7..76dec81b6 100644 --- a/Cli/Manager/Cli-Manager.csproj +++ b/Cli/Manager/Cli-Manager.csproj @@ -6,7 +6,8 @@ Properties git-credential-manager true - + + Exe {19770407-63D4-40A8-A9DF-F1C4B473308A} Cli-Manager @@ -24,10 +25,25 @@ get + + ..\..\packages\Microsoft.Azure.Services.AppAuthentication.1.0.3\lib\net452\Microsoft.Azure.Services.AppAuthentication.dll + + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.4.5.0\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + + + + + + + + @@ -57,6 +73,9 @@ false + + Designer + @@ -67,5 +86,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + \ No newline at end of file diff --git a/Cli/Manager/app.config b/Cli/Manager/app.config index ff9950103..be876fec2 100644 --- a/Cli/Manager/app.config +++ b/Cli/Manager/app.config @@ -1,3 +1,20 @@ - + - + + + + + + + + + + + + + + + + + + diff --git a/Cli/Manager/packages.config b/Cli/Manager/packages.config index 644ec0093..e2e3ccbae 100644 --- a/Cli/Manager/packages.config +++ b/Cli/Manager/packages.config @@ -1,5 +1,7 @@  + + diff --git a/Cli/Test/ProgramTests.cs b/Cli/Test/ProgramTests.cs index d76616ef6..b9d441d32 100644 --- a/Cli/Test/ProgramTests.cs +++ b/Cli/Test/ProgramTests.cs @@ -29,7 +29,7 @@ using Microsoft.Alm.Authentication; using Moq; using Xunit; -using Azure = AzureDevOps.Authentication; +using AzureDev = AzureDevOps.Authentication; using Git = Microsoft.Alm.Authentication.Git; namespace Microsoft.Alm.Cli.Test @@ -108,7 +108,7 @@ public async Task LoadOperationArgumentsTest() Assert.NotNull(opargs.DevOpsTokenScope); - var expectedScope = Azure.TokenScope.BuildAccess | Azure.TokenScope.CodeWrite; + var expectedScope = AzureDev.TokenScope.BuildAccess | AzureDev.TokenScope.CodeWrite; Assert.Equal(expectedScope, opargs.DevOpsTokenScope); } diff --git a/Cli/Test/app.config b/Cli/Test/app.config index b546f120b..a7de87bc0 100644 --- a/Cli/Test/app.config +++ b/Cli/Test/app.config @@ -10,6 +10,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/GitHub.Authentication/Proxy/GitHub.Authentication.Proxy.csproj b/GitHub.Authentication/Proxy/GitHub.Authentication.Proxy.csproj index 5a487378e..3b4357bbc 100644 --- a/GitHub.Authentication/Proxy/GitHub.Authentication.Proxy.csproj +++ b/GitHub.Authentication/Proxy/GitHub.Authentication.Proxy.csproj @@ -31,8 +31,8 @@ ..\..\packages\Moq.4.8.2\lib\net45\Moq.dll - - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll diff --git a/GitHub.Authentication/Src/GitHub.Authentication.csproj b/GitHub.Authentication/Src/GitHub.Authentication.csproj index b6b6d7769..9ec7d3af0 100644 --- a/GitHub.Authentication/Src/GitHub.Authentication.csproj +++ b/GitHub.Authentication/Src/GitHub.Authentication.csproj @@ -12,7 +12,7 @@ GitHub.Authentication {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} GitHub.Authentication - v4.5.1 + v4.5.2 diff --git a/GitHub.Authentication/Test/AuthenticationTests.cs b/GitHub.Authentication/Test/AuthenticationTests.cs index 922ce4f08..a61aef372 100644 --- a/GitHub.Authentication/Test/AuthenticationTests.cs +++ b/GitHub.Authentication/Test/AuthenticationTests.cs @@ -27,7 +27,7 @@ public async Task GetSetCredentialsNormalizesGistUrls(string writeUriString, str new Authentication.AcquireAuthenticationCodeDelegate(prompts.AuthenticationCodeModalPrompt), null); - await authentication.SetCredentials(new Uri(writeUriString), new Credential("haacked")); + await authentication.SetCredentials(new Uri(writeUriString), new Credential("haacked", string.Empty)); var credentials = await authentication.GetCredentials(retrieveUri); Assert.Equal("haacked", credentials.Username); } @@ -48,7 +48,7 @@ public async Task GetSetCredentialsDoesNotReturnCredentialForRandomUrl() new Authentication.AcquireAuthenticationCodeDelegate(prompts.AuthenticationCodeModalPrompt), null); - await authentication.SetCredentials(new Uri("https://github.com/"), new Credential("haacked")); + await authentication.SetCredentials(new Uri("https://github.com/"), new Credential("haacked", string.Empty)); Assert.Null(await authentication.GetCredentials(retrieveUri)); } diff --git a/Microsoft.Alm.Authentication/Proxy/Microsoft.Alm.Authentication.Proxy.csproj b/Microsoft.Alm.Authentication/Proxy/Microsoft.Alm.Authentication.Proxy.csproj index fe11bc87f..500bcf67f 100644 --- a/Microsoft.Alm.Authentication/Proxy/Microsoft.Alm.Authentication.Proxy.csproj +++ b/Microsoft.Alm.Authentication/Proxy/Microsoft.Alm.Authentication.Proxy.csproj @@ -23,8 +23,8 @@ ..\..\packages\Moq.4.8.2\lib\net45\Moq.dll - - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll diff --git a/Microsoft.Alm.Authentication/Src/Credential.cs b/Microsoft.Alm.Authentication/Src/Credential.cs index 5d04b80e0..fddc7f7a2 100644 --- a/Microsoft.Alm.Authentication/Src/Credential.cs +++ b/Microsoft.Alm.Authentication/Src/Credential.cs @@ -42,25 +42,17 @@ public sealed class Credential : Secret, IEquatable /// /// Creates a credential object with a username and password pair. /// - /// The username value of the ``. - /// The password value of the ``. - public Credential(string username, string password) + /// The username value of the ``. + /// The password value of the ``. + public Credential(string Username, string Password) { - if (username is null) - throw new ArgumentNullException(nameof(username)); + if (Username is null) + throw new ArgumentNullException(nameof(Username)); - _password = password ?? string.Empty; - _username = username; + _password = Password ?? string.Empty; + _username = Username; } - /// - /// Creates a credential object with only a username. - /// - /// The username value of the ``. - public Credential(string username) - : this(username, string.Empty) - { } - private readonly string _password; private readonly string _username; diff --git a/Microsoft.Alm.Authentication/Src/KeyVaultHelper.cs b/Microsoft.Alm.Authentication/Src/KeyVaultHelper.cs new file mode 100644 index 000000000..b3369c7e3 --- /dev/null +++ b/Microsoft.Alm.Authentication/Src/KeyVaultHelper.cs @@ -0,0 +1,256 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ + +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.Azure.Services.AppAuthentication; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace Microsoft.Azure.KeyVault.Helper +{ + /// + /// This class provides access to secrets stored in the KeyVault + /// + public sealed class KeyVaultHelper : IDisposable + { + public struct Config + { + public string CertificateThumbprint { get; set; } + public string CertificateStoreType { get; set; } + public string KeyVaultUrl { get; set; } + public string ClientId { get; set; } + public bool? UseMsi { get; set; } + } + + private static KeyVaultHelper _instance = null; + private static KeyVaultHelper.Config _config; + + private readonly string _keyVaultUrl; + private readonly string _clientId; + private readonly string _certificateThumbprint; + private readonly StoreLocation _storeLocation; + private readonly bool? _useMsi; + private readonly KeyVaultClient _keyVaultClient; + private string _accessToken; + private DateTimeOffset _expiration; + + private KeyVaultHelper() + { + _keyVaultUrl = _config.KeyVaultUrl; + if (string.IsNullOrWhiteSpace(_keyVaultUrl)) + { + throw new KeyVaultHelperConfigurationException("KeyVault URL is not set in the git config file"); + } + + _useMsi = _config.UseMsi; + if (!_useMsi.HasValue) + { + _useMsi = false; + } + + // using MSI (Managed Service Identity) method of authentication means we dont need any service principal arguments - appId, certificate or cert store + if (_useMsi == true) + { + var azureServiceTokenProvider = new AzureServiceTokenProvider(); + _keyVaultClient = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback)); + } + else + { + _certificateThumbprint = _config.CertificateThumbprint; + if (string.IsNullOrWhiteSpace(_certificateThumbprint)) + { + throw new KeyVaultHelperConfigurationException("Certificate thumbprint is required for Azure AD application authentication. Please set KeyVaultAuthCertificateThumbprint setting in the git config file"); + } + _certificateThumbprint = _certificateThumbprint.Replace(" ", "").Trim(); + + string certificateStoreType = _config.CertificateStoreType; + if (string.IsNullOrWhiteSpace(certificateStoreType)) + { + throw new KeyVaultHelperConfigurationException("Certificate store type (CurrentUser or LocalMachine) is required for KeyVault access. Please set KeyVaultAuthCertificateStoreType setting in the git config file"); + } + + if (!Enum.TryParse(certificateStoreType, true, out _storeLocation)) + throw new KeyVaultHelperConfigurationException("Invalid certificate store type in git config file:" + certificateStoreType); + + _clientId = _config.ClientId; + if (string.IsNullOrWhiteSpace(_clientId)) + { + throw new KeyVaultHelperConfigurationException("Client Id (applicationID) for KeyVault App is not set in the git config file"); + } + + _keyVaultClient = new KeyVaultClient(GetAccessToken); + } + } + + public static void Configure(KeyVaultHelper.Config config) + { + // validate and clean up config + if (string.IsNullOrWhiteSpace(config.KeyVaultUrl)) + { + throw new KeyVaultHelperConfigurationException("KeyVault URL is not set in the git config file"); + } + + config.KeyVaultUrl = config.KeyVaultUrl.Trim(); + + // if msi is not specified or value is false retrieve the cert details + if (config.UseMsi != true) + { + if (string.IsNullOrWhiteSpace(config.CertificateThumbprint)) + { + throw new KeyVaultHelperConfigurationException("Certificate thumbprint is required for Azure AD application authentication. Please set KeyVaultAuthCertificateThumbprint setting in the git config file"); + } + + config.CertificateThumbprint = config.CertificateThumbprint.Replace(" ", "").Trim(); + + if (string.IsNullOrWhiteSpace(config.CertificateStoreType)) + { + throw new KeyVaultHelperConfigurationException("Certificate store type (CurrentUser or LocalMachine) is required for KeyVault access. Please set KeyVaultAuthCertificateStoreType setting in the git config file"); + } + + config.CertificateStoreType = config.CertificateStoreType.Trim(); + StoreLocation storeLocation; + if (!Enum.TryParse(config.CertificateStoreType, true, out storeLocation)) + throw new KeyVaultHelperConfigurationException("Invalid certificate store type in the git config file:" + config.CertificateStoreType); + + if (string.IsNullOrWhiteSpace(config.ClientId)) + { + throw new KeyVaultHelperConfigurationException("Client Id (applicationID) for KeyVault App is not set in the git config file"); + } + config.ClientId = config.ClientId.Trim(); + } + _config = config; + } + + public static KeyVaultHelper KeyVault + { + get + { + if (_instance == null) + _instance = new KeyVaultHelper(); + return _instance; + } + } + + public async Task GetSecretAsync(string secretName) + { + if (string.IsNullOrEmpty(secretName)) + { + throw new ArgumentNullException("secretName"); + } + + string fixedSecretName = FixSecretName(secretName); + + var secret = await _keyVaultClient.GetSecretAsync(_keyVaultUrl, fixedSecretName).ConfigureAwait(false); + + return secret.Value; + } + + public async Task SetSecretAsync(string secretName, string secretValue) + { + if (string.IsNullOrEmpty(secretName)) + { + throw new ArgumentNullException("secretName"); + } + // KeyVault doesn't allow dots in key name, allows only alphanumeric and dashes. Replace dots with dash + string fixedSecretName = FixSecretName(secretName); + + var secret = await _keyVaultClient.SetSecretAsync(_keyVaultUrl, fixedSecretName, secretValue); + return secret.Value; + } + + public async Task DeleteSecretAsync(string secretName) + { + if (string.IsNullOrEmpty(secretName)) + { + throw new ArgumentNullException("secretName"); + } + // KeyVault doesn't allow dots in key name, allows only alphanumeric and dashes. Replace dots with dash + string fixedSecretName = FixSecretName (secretName); + + var deletedSecretResult = await _keyVaultClient.DeleteSecretAsync(_keyVaultUrl, fixedSecretName); + return deletedSecretResult.Value; + } + + private static string FixSecretName (string secretName) + { + // KeyVault doesn't allow dots or slashes in key name, allows only alphanumeric and dashes. Replace dots and slashes with dash + return secretName.Replace('.', '-').Replace('/', '-').Replace(' ', '-'); ; + } + private async Task GetAccessToken(string authority, string resource, string scope) + { + // get new token if needed or current token expires soon + if (_accessToken == null || + _expiration == null || + _expiration.UtcDateTime > DateTime.Now.AddMinutes(1).ToUniversalTime()) + { + var context = new AuthenticationContext(authority); + var cert = RetrieveCertificate(); + var clientAssertionCertificate = new ClientAssertionCertificate(_clientId, cert); + + var result = await context.AcquireTokenAsync(resource, clientAssertionCertificate); + _expiration = result.ExpiresOn; + _accessToken = result.AccessToken; + } + return _accessToken; + } + + private X509Certificate2 RetrieveCertificate() + { + X509Store certStore = null; + try + { + certStore = new X509Store(_storeLocation); + certStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + var userCertCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, _certificateThumbprint, false); + + if (userCertCollection?.Count == 0) + { + throw new KeyVaultHelperConfigurationException( + $"Certificate with thumbprint '{_certificateThumbprint}' not found in store '{certStore.Location}/{certStore.Name}'"); + } + return userCertCollection[0]; + } + catch (KeyVaultHelperConfigurationException) + { + throw; + } + catch (Exception ex) + { + throw new KeyVaultHelperConfigurationException( + $"An error occurred accessing the '{_storeLocation}' certificate store.", ex); + } + finally + { + certStore?.Close(); + } + } + + // Implement IDisposable. + public void Dispose() + { + Dispose(true); + // This object will be cleaned up by the Dispose method. + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (_keyVaultClient != null && _instance != null) + { + // If disposing equals true, dispose all managed + // and unmanaged resources. + if (disposing) + { + _keyVaultClient.Dispose(); + } + } + _instance = null; + } + } +} diff --git a/Microsoft.Alm.Authentication/Src/KeyVaultHelperConfigurationException.cs b/Microsoft.Alm.Authentication/Src/KeyVaultHelperConfigurationException.cs new file mode 100644 index 000000000..83b754796 --- /dev/null +++ b/Microsoft.Alm.Authentication/Src/KeyVaultHelperConfigurationException.cs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ------------------------------------------------------------------------------ + +using System; + +namespace Microsoft.Azure.KeyVault.Helper +{ + /// + /// This exception is thrown when KeyVaultHelper class can't read configuration from + /// config file or can't access cerificate store and retrieve certificate for KeyVault access. + /// + [Serializable] + public class KeyVaultHelperConfigurationException : Exception + { + public KeyVaultHelperConfigurationException(string message) + : base(message) + { + } + + public KeyVaultHelperConfigurationException(string message, Exception ex) + : base(message + " See inner exception for details.", ex) + { + } + } + +} diff --git a/Microsoft.Alm.Authentication/Src/KeyVaultSecretStore.cs b/Microsoft.Alm.Authentication/Src/KeyVaultSecretStore.cs new file mode 100644 index 000000000..dc985d0cd --- /dev/null +++ b/Microsoft.Alm.Authentication/Src/KeyVaultSecretStore.cs @@ -0,0 +1,220 @@ +/**** Git Credential Manager for Windows **** + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the """"Software""""), to deal + * in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." +**/ + +using Newtonsoft.Json; +using System; +using System.Threading.Tasks; +using Microsoft.Azure.KeyVault.Helper; + +namespace Microsoft.Alm.Authentication +{ + public class KeyVaultSecretStore : ICredentialStore + { + private readonly RuntimeContext _context; + private Secret.UriNameConversionDelegate _getTargetName; + private readonly string _namespace; + private readonly ICredentialStore _credentialCache; + + protected Git.ITrace Trace + => _context.Trace; + + public KeyVaultSecretStore(RuntimeContext context, + string @namespace, + string keyVaultUrl, + bool? useMsi, + string certAuthStoreType, + string certAuthThumbprint, + string certAuthClientId) : + this (context, @namespace, null, keyVaultUrl, useMsi, certAuthStoreType, certAuthThumbprint, certAuthClientId, null) + { } + + public KeyVaultSecretStore(RuntimeContext context, + string @namespace, + ICredentialStore credentialCache, + string keyVaultUrl, + bool? useMsi, + string certAuthStoreType, + string certAuthThumbprint, + string certAuthClientId, + Secret.UriNameConversionDelegate getTargetName) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + _context = context; + + if (@namespace is null) + throw new ArgumentNullException(nameof(@namespace)); + + if (@namespace.IndexOfAny(BaseSecureStore.IllegalCharacters) != -1) + { + var inner = new FormatException("Namespace contains illegal characters."); + throw new ArgumentException(inner.Message, nameof(@namespace), inner); + } + + _getTargetName = getTargetName ?? Secret.UriToName; + + _namespace = @namespace; + _credentialCache = credentialCache ?? new SecretCache(context, @namespace, _getTargetName); + this._getTargetName = getTargetName; + + KeyVaultHelper.Config config = new KeyVaultHelper.Config() + { + KeyVaultUrl = keyVaultUrl, + UseMsi = useMsi, + CertificateThumbprint = certAuthThumbprint, + CertificateStoreType = certAuthStoreType, + ClientId = certAuthClientId + }; + + KeyVaultHelper.Configure(config); + } + public string Namespace + { + get { return _namespace; } + } + + public Secret.UriNameConversionDelegate UriNameConversion + { + get { return _getTargetName; } + set + { + if (value is null) + throw new ArgumentNullException(nameof(UriNameConversion)); + + _getTargetName = value; + } + } + + public async Task DeleteCredentials(TargetUri targetUri) + { +#if DEBUG + Trace.WriteLine($"targetUri: '{targetUri}': Key: '{GetKeyVaultKey(targetUri)}'"); +#endif + + if (targetUri is null || string.IsNullOrEmpty(targetUri.Host)) + throw new ArgumentNullException(nameof(targetUri)); + + string secret = null; + try + { + secret = await KeyVaultHelper.KeyVault.DeleteSecretAsync(GetKeyVaultKey(targetUri)); + } + catch (Exception ex) + { + Trace.WriteLine("Exception deleting the secret from KeyVault:" + ex.Message); + } + + return string.IsNullOrEmpty(secret) + && await _credentialCache.DeleteCredentials(targetUri); + } + + public async Task ReadCredentials(TargetUri targetUri) + { +#if DEBUG + Trace.WriteLine($"targetUri: '{targetUri}': Key: '{GetKeyVaultKey(targetUri)}'"); +#endif + if (targetUri is null || string.IsNullOrEmpty(targetUri.Host)) + throw new ArgumentNullException(nameof(targetUri)); + + return await _credentialCache.ReadCredentials(targetUri) + ?? await this.ReadKeyVaultCredentials(targetUri); + } + + public async Task WriteCredentials(TargetUri targetUri, Credential credentials) + { +#if DEBUG + Trace.WriteLine($"targetUri: '{targetUri}', userName: '{credentials?.Username}' PAT: '{credentials?.Password}', : Key: '{GetKeyVaultKey(targetUri)}'"); +#endif + if (targetUri is null || string.IsNullOrEmpty (targetUri.Host)) + throw new ArgumentNullException(nameof(targetUri)); + + if (credentials is null) + throw new ArgumentNullException(nameof(credentials)); + return await WriteKeyVaultCredentials(targetUri, credentials) + && await _credentialCache.WriteCredentials(targetUri, credentials); + } + + private async Task ReadKeyVaultCredentials(TargetUri targetUri) + { + string secret = null; + try + { + secret = await KeyVaultHelper.KeyVault.GetSecretAsync(GetKeyVaultKey(targetUri)); + } + catch (Exception ex) + { + Trace.WriteLine("Exception getting the secret from KeyVault:" + ex.Message); + } + + if (string.IsNullOrEmpty(secret)) + { + return null; + } + + // parse secret from JSon + try + { + Credential credential = JsonConvert.DeserializeObject(secret); + return credential; + } + catch (JsonException) + { + Trace.WriteLine("Keyvault secret doesn't contain Json value, returning as is"); + return new Credential("PersonalAccessToken", secret); + } + } + + private async Task WriteKeyVaultCredentials(TargetUri targetUri, Credential credentials) + { + string secret = "{ \"Username\" : \"" + credentials.Username + "\", \"Password\" : \"" + credentials.Password + "\", \"Message\" : \"\" }"; + try + { + await KeyVaultHelper.KeyVault.SetSecretAsync(GetKeyVaultKey(targetUri), secret); + return true; + } + catch (Exception ex) + { + Trace.WriteLine("Exception writing a secret to KeyVault:" + ex.Message); + } + return false; + } + + private static string GetKeyVaultKey (TargetUri targetUri) + { + string path = null; + + if (targetUri.HasPath && !string.IsNullOrEmpty(targetUri.AbsolutePath)) + path = targetUri.Host + targetUri.AbsolutePath; + else if (targetUri.ContainsUserInfo && !string.IsNullOrEmpty(targetUri.UserInfo)) + path = targetUri.UserInfo + "-" + targetUri.Host; + else + path = targetUri.Host; + + string key = path.Replace('.', '-').Replace('/', '-').Replace(' ', '-'); + return key; + } + + } +} \ No newline at end of file diff --git a/Microsoft.Alm.Authentication/Src/Microsoft.Alm.Authentication.csproj b/Microsoft.Alm.Authentication/Src/Microsoft.Alm.Authentication.csproj index 9ce86dcbe..a5363cd4e 100644 --- a/Microsoft.Alm.Authentication/Src/Microsoft.Alm.Authentication.csproj +++ b/Microsoft.Alm.Authentication/Src/Microsoft.Alm.Authentication.csproj @@ -5,12 +5,14 @@ Properties Microsoft.Alm.Authentication - + + Library {19770407-B493-459D-BB4F-04FBEFB1BA13} Microsoft.Alm.Authentication Microsoft.Alm.Authentication - v4.5.1 + v4.5.2 + @@ -18,16 +20,56 @@ $(ProjectDir)$(OutputPath) + + ..\..\packages\Hyak.Common.1.2.2\lib\net452\Hyak.Common.dll + + + ..\..\packages\Microsoft.Azure.Common.2.2.1\lib\net452\Microsoft.Azure.Common.dll + + + ..\..\packages\Microsoft.Azure.KeyVault.3.0.3\lib\net452\Microsoft.Azure.KeyVault.dll + + + ..\..\packages\Microsoft.Azure.KeyVault.WebKey.3.0.3\lib\net452\Microsoft.Azure.KeyVault.WebKey.dll + + + ..\..\packages\Microsoft.Azure.Services.AppAuthentication.1.0.3\lib\net452\Microsoft.Azure.Services.AppAuthentication.dll + + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.4.5.0\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\..\packages\Microsoft.Rest.ClientRuntime.2.3.18\lib\net452\Microsoft.Rest.ClientRuntime.dll + + + ..\..\packages\Microsoft.Rest.ClientRuntime.Azure.3.3.18\lib\net452\Microsoft.Rest.ClientRuntime.Azure.dll + + + ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + @@ -63,8 +105,10 @@ - - + + Designer + + @@ -74,5 +118,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + \ No newline at end of file diff --git a/Microsoft.Alm.Authentication/Src/app.config b/Microsoft.Alm.Authentication/Src/app.config index d5a97e2b1..bc8302afd 100644 --- a/Microsoft.Alm.Authentication/Src/app.config +++ b/Microsoft.Alm.Authentication/Src/app.config @@ -4,8 +4,12 @@ - + + + + + - + diff --git a/Microsoft.Alm.Authentication/Src/packages.config b/Microsoft.Alm.Authentication/Src/packages.config index 339420105..4e1484912 100644 --- a/Microsoft.Alm.Authentication/Src/packages.config +++ b/Microsoft.Alm.Authentication/Src/packages.config @@ -1,4 +1,13 @@  + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Alm.Authentication/Test/app.config b/Microsoft.Alm.Authentication/Test/app.config index 6caa983bd..d77de4848 100644 --- a/Microsoft.Alm.Authentication/Test/app.config +++ b/Microsoft.Alm.Authentication/Test/app.config @@ -4,11 +4,11 @@ - + - + diff --git a/Shared/Cli/Functions/Common.cs b/Shared/Cli/Functions/Common.cs index 5acdf04e2..08c3afc3f 100644 --- a/Shared/Cli/Functions/Common.cs +++ b/Shared/Cli/Functions/Common.cs @@ -29,7 +29,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Alm.Authentication; -using Azure = AzureDevOps.Authentication; +using AzureDev = AzureDevOps.Authentication; using Bitbucket = Atlassian.Bitbucket.Authentication; using Git = Microsoft.Alm.Authentication.Git; using Github = GitHub.Authentication; @@ -85,25 +85,25 @@ public static async Task CreateAuthentication(Program progra program.Trace.WriteLine($"detecting authority type for '{operationArguments.TargetUri}'."); // Detect the authority. - authority = await Azure.Authentication.GetAuthentication(program.Context, + authority = await AzureDev.Authentication.GetAuthentication(program.Context, operationArguments.TargetUri, Program.DevOpsCredentialScope, - new SecretStore(program.Context, - secretsNamespace, - Azure.Authentication.UriNameConversion)) + CreateSecretStore(program.Context, + operationArguments, + AzureDev.Authentication.UriNameConversion)) ?? Github.Authentication.GetAuthentication(program.Context, operationArguments.TargetUri, Program.GitHubCredentialScope, - new SecretStore(program.Context, - secretsNamespace, + CreateSecretStore(program.Context, + operationArguments, Secret.UriToName), githubCredentialCallback, githubAuthcodeCallback, null) ?? Bitbucket.Authentication.GetAuthentication(program.Context, operationArguments.TargetUri, - new SecretStore(program.Context, - secretsNamespace, + CreateSecretStore(program.Context, + operationArguments, Secret.UriToIdentityUrl), bitbucketCredentialCallback, bitbucketOauthCallback); @@ -111,12 +111,12 @@ public static async Task CreateAuthentication(Program progra if (authority != null) { // Set the authority type based on the returned value. - if (authority is Azure.MsaAuthentication) + if (authority is AzureDev.MsaAuthentication) { operationArguments.Authority = AuthorityType.MicrosoftAccount; goto case AuthorityType.MicrosoftAccount; } - else if (authority is Azure.AadAuthentication) + else if (authority is AzureDev.AadAuthentication) { operationArguments.Authority = AuthorityType.AzureDirectory; goto case AuthorityType.AzureDirectory; @@ -144,7 +144,7 @@ public static async Task CreateAuthentication(Program progra Guid tenantId = Guid.Empty; // Get the identity of the tenant. - var result = await Azure.Authentication.DetectAuthority(program.Context, operationArguments.TargetUri); + var result = await AzureDev.Authentication.DetectAuthority(program.Context, operationArguments.TargetUri); if (result.HasValue) { @@ -152,12 +152,12 @@ public static async Task CreateAuthentication(Program progra } // Create the authority object. - authority = new Azure.AadAuthentication(program.Context, + authority = new AzureDev.AadAuthentication(program.Context, tenantId, operationArguments.DevOpsTokenScope, - new SecretStore(program.Context, - secretsNamespace, - Azure.AadAuthentication.UriNameConversion)); + CreateSecretStore(program.Context, + operationArguments, + AzureDev.AadAuthentication.UriNameConversion)); } // Return the allocated authority or a generic AAD backed Azure DevOps authentication object. @@ -179,8 +179,8 @@ public static async Task CreateAuthentication(Program progra return authority ?? new Github.Authentication(program.Context, operationArguments.TargetUri, Program.GitHubCredentialScope, - new SecretStore(program.Context, - secretsNamespace, + CreateSecretStore(program.Context, + operationArguments, Secret.UriToName), githubCredentialCallback, githubAuthcodeCallback, @@ -193,8 +193,8 @@ public static async Task CreateAuthentication(Program progra // Return a Bitbucket authentication object. return authority ?? new Bitbucket.Authentication(program.Context, - new SecretStore(program.Context, - secretsNamespace, + CreateSecretStore(program.Context, + operationArguments, Secret.UriToIdentityUrl), bitbucketCredentialCallback, bitbucketOauthCallback); @@ -205,11 +205,11 @@ public static async Task CreateAuthentication(Program progra program.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Microsoft Live."); // Return the allocated authority or a generic MSA backed Azure DevOps authentication object. - return authority ?? new Azure.MsaAuthentication(program.Context, + return authority ?? new AzureDev.MsaAuthentication(program.Context, operationArguments.DevOpsTokenScope, - new SecretStore(program.Context, - secretsNamespace, - Azure.MsaAuthentication.UriNameConversion)); + CreateSecretStore(program.Context, + operationArguments, + AzureDev.MsaAuthentication.UriNameConversion)); } case AuthorityType.Ntlm: @@ -225,8 +225,8 @@ public static async Task CreateAuthentication(Program progra // Return a generic username + password authentication object. return authority ?? new BasicAuthentication(program.Context, - new SecretStore(program.Context, - secretsNamespace, + CreateSecretStore(program.Context, + operationArguments, Secret.UriToIdentityUrl), basicNtlmSupport, basicCredentialCallback, @@ -235,6 +235,20 @@ public static async Task CreateAuthentication(Program progra } } + private static ICredentialStore CreateSecretStore (RuntimeContext context, OperationArguments operationArguments, Secret.UriNameConversionDelegate getTargetName) + { + string secretsNamespace = operationArguments.CustomNamespace ?? Program.SecretsNamespace; + + if (string.IsNullOrEmpty(operationArguments.KeyVaultUrl)) + { + return new SecretStore(context, secretsNamespace, getTargetName); + } + else + { + return new KeyVaultSecretStore(context, secretsNamespace, operationArguments.KeyVaultUrl, operationArguments.KeyVaultUseMsi, operationArguments.KeyVaulyAuthCertificateStoreType, operationArguments.KeyVaultAuthCertificateThumbprint, operationArguments.KeyVaultAuthClientId); + } + } + public static async Task DeleteCredentials(Program program, OperationArguments operationArguments) { if (program is null) @@ -257,7 +271,7 @@ public static async Task DeleteCredentials(Program program, OperationArgum case AuthorityType.MicrosoftAccount: { program.Trace.WriteLine($"deleting Azure DevOps credentials for '{operationArguments.TargetUri}'."); - var adoAuth = authentication as Azure.Authentication; + var adoAuth = authentication as AzureDev.Authentication; return await adoAuth.DeleteCredentials(operationArguments.TargetUri); } @@ -607,14 +621,14 @@ public static async Task LoadOperationArguments(Program program, OperationArgume { program.Trace.WriteLine($"{program.KeyTypeName(KeyType.DevOpsScope)} = '{value}'."); - Azure.TokenScope devopsTokenScope = Azure.TokenScope.None; + AzureDev.TokenScope devopsTokenScope = AzureDev.TokenScope.None; var scopes = value.Split(TokenScopeSeparatorCharacters.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < scopes.Length; i += 1) { scopes[i] = scopes[i].Trim(); - if (Azure.TokenScope.Find(scopes[i], out Azure.TokenScope scope)) + if (AzureDev.TokenScope.Find(scopes[i], out AzureDev.TokenScope scope)) { devopsTokenScope = devopsTokenScope | scope; } @@ -631,14 +645,14 @@ public static async Task LoadOperationArguments(Program program, OperationArgume program.Trace.WriteLine($"GCM_VSTS_SCOPE = '{value}'."); program.WriteLine($"WARNING: the 'GCM_VSTS_SCOPE' variable has been deprecated, use 'GCM_DEVOPS_SCOPE' instead."); - Azure.TokenScope devopsTokenScope = Azure.TokenScope.None; + AzureDev.TokenScope devopsTokenScope = AzureDev.TokenScope.None; var scopes = value.Split(TokenScopeSeparatorCharacters.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < scopes.Length; i += 1) { scopes[i] = scopes[i].Trim(); - if (Azure.TokenScope.Find(scopes[i], out Azure.TokenScope scope)) + if (AzureDev.TokenScope.Find(scopes[i], out AzureDev.TokenScope scope)) { devopsTokenScope = devopsTokenScope | scope; } @@ -652,7 +666,7 @@ public static async Task LoadOperationArguments(Program program, OperationArgume } // Check for configuration supplied user-info. - if (program.TryReadString(operationArguments, KeyType.Username, out value)) + if (program.TryReadString(operationArguments, KeyType.Username, out value)) { program.Trace.WriteLine($"{program.KeyTypeName(KeyType.Username)} = '{value}'."); @@ -695,6 +709,37 @@ public static async Task LoadOperationArguments(Program program, OperationArgume Global.RequestTimeout = milliseconds; } } + + // KeyVault Credential Store Parameters + if (program.TryReadString(operationArguments, KeyType.KeyVaultUrl, out value)) + { + program.Trace.WriteLine($"{program.KeyTypeName(KeyType.KeyVaultUrl)} = '{value}'."); + operationArguments.KeyVaultUrl = value; + } + + if (program.TryReadBoolean(operationArguments, KeyType.KeyVaultUseMsi, out yesno)) + { + program.Trace.WriteLine($"{program.KeyTypeName(KeyType.KeyVaultUseMsi)} = '{yesno}'."); + operationArguments.KeyVaultUseMsi = yesno; + } + + if (program.TryReadString(operationArguments, KeyType.KeyVaultAuthCertificateStoreType, out value)) + { + program.Trace.WriteLine($"{program.KeyTypeName(KeyType.KeyVaultAuthCertificateStoreType)} = '{value}'."); + operationArguments.KeyVaulyAuthCertificateStoreType = value; + } + + if (program.TryReadString(operationArguments, KeyType.KeyVaultAuthCertificateThumbprint, out value)) + { + program.Trace.WriteLine($"{program.KeyTypeName(KeyType.KeyVaultAuthCertificateThumbprint)} = '{value}'."); + operationArguments.KeyVaultAuthCertificateThumbprint = value; + } + + if (program.TryReadString(operationArguments, KeyType.KeyVaultAuthClientId, out value)) + { + program.Trace.WriteLine($"{program.KeyTypeName(KeyType.KeyVaultAuthClientId)} = '{value}'."); + operationArguments.KeyVaultAuthClientId = value; + } } public static void LogEvent(Program program, string message, EventLogEntryType eventType) @@ -791,8 +836,8 @@ public static async Task QueryCredentials(Program program, Operation case AuthorityType.AzureDirectory: { - var aadAuth = authentication as Azure.AadAuthentication; - var patOptions = new Azure.PersonalAccessTokenOptions() + var aadAuth = authentication as AzureDev.AadAuthentication; + var patOptions = new AzureDev.PersonalAccessTokenOptions() { RequireCompactToken = true, TokenDuration = operationArguments.TokenDuration, @@ -827,8 +872,8 @@ public static async Task QueryCredentials(Program program, Operation case AuthorityType.MicrosoftAccount: { - var msaAuth = authentication as Azure.MsaAuthentication; - var patOptions = new Azure.PersonalAccessTokenOptions() + var msaAuth = authentication as AzureDev.MsaAuthentication; + var patOptions = new AzureDev.PersonalAccessTokenOptions() { RequireCompactToken = true, TokenDuration = operationArguments.TokenDuration, diff --git a/Shared/Cli/OperationArguments.cs b/Shared/Cli/OperationArguments.cs index 38b719523..4b065e555 100644 --- a/Shared/Cli/OperationArguments.cs +++ b/Shared/Cli/OperationArguments.cs @@ -30,7 +30,7 @@ using System.Threading.Tasks; using Microsoft.Alm.Authentication; using static System.Globalization.CultureInfo; -using Azure = AzureDevOps.Authentication; +using AzureDev = AzureDevOps.Authentication; using Git = Microsoft.Alm.Authentication.Git; namespace Microsoft.Alm.Cli @@ -69,7 +69,7 @@ public OperationArguments() private Git.Configuration _configuration; private Credential _credentials; private string _customNamespace; - private Azure.TokenScope _devopsTokenScope; + private AzureDev.TokenScope _devopsTokenScope; private Dictionary _environmentVariables; private string _gitRemoteHttpCommandLine; private Interactivity _interactivity; @@ -128,7 +128,7 @@ public virtual string CustomNamespace /// /// Default value is ``. /// - public virtual Azure.TokenScope DevOpsTokenScope + public virtual AzureDev.TokenScope DevOpsTokenScope { get { return _devopsTokenScope; } set { _devopsTokenScope = value; } @@ -445,6 +445,20 @@ public virtual bool WriteLog set { _writeLog = value; } } + /// + /// KeyVaultUrl for storing credentials and PAT tokens in KeyVault. + /// If specified, KeyVault is used as a storage mechanism for secrets. + /// + public virtual string KeyVaultUrl { get; set; } + + public virtual bool? KeyVaultUseMsi { get; set; } + + public virtual string KeyVaulyAuthCertificateStoreType { get; set; } + + public virtual string KeyVaultAuthCertificateThumbprint { get; set; } + + public virtual string KeyVaultAuthClientId { get; set; } + internal string DebuggerDisplay { get { return $"{nameof(OperationArguments)}: TargetUri: {TargetUri}, Authority: {Authority}, Credentials: {Credentials}"; } @@ -655,20 +669,29 @@ public override string ToString() .Append(_queryPath ?? string.Empty) .Append('\n'); - // Only write out username if we know it. - if (_credentials?.Username != null) + if (_credentials == null) { builder.Append("username=") - .Append(_credentials.Username) - .Append('\n'); + .Append(_username ?? string.Empty) + .Append('\n'); } - - // Only write out password if we know it. - if (_credentials?.Password != null) + else { - builder.Append("password=") - .Append(_credentials.Password) - .Append('\n'); + // Only write out username if we know it. + if (_credentials.Username != null) + { + builder.Append("username=") + .Append(_credentials.Username) + .Append('\n'); + } + + // Only write out password if we know it. + if (_credentials.Password != null) + { + builder.Append("password=") + .Append(_credentials.Password) + .Append('\n'); + } } builder.Append('\n'); diff --git a/Shared/Cli/Program.cs b/Shared/Cli/Program.cs index 215ede353..0b8ab8ce4 100644 --- a/Shared/Cli/Program.cs +++ b/Shared/Cli/Program.cs @@ -32,7 +32,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Alm.Authentication; -using Azure = AzureDevOps.Authentication; +using AzureDev = AzureDevOps.Authentication; using Bitbucket = Atlassian.Bitbucket.Authentication; using Git = Microsoft.Alm.Authentication.Git; using Github = GitHub.Authentication; @@ -61,6 +61,11 @@ enum KeyType ParentHwnd, VstsScope, Writelog, + KeyVaultUrl, + KeyVaultUseMsi, + KeyVaultAuthCertificateThumbprint, + KeyVaultAuthCertificateStoreType, + KeyVaultAuthClientId } partial class Program @@ -79,7 +84,7 @@ partial class Program internal const string ConfigPrefix = "credential"; internal const string SecretsNamespace = "git"; - internal static readonly Azure.TokenScope DevOpsCredentialScope = Azure.TokenScope.CodeWrite | Azure.TokenScope.PackagingRead; + internal static readonly AzureDev.TokenScope DevOpsCredentialScope = AzureDev.TokenScope.CodeWrite | AzureDev.TokenScope.PackagingRead; internal static readonly Github.TokenScope GitHubCredentialScope = Github.TokenScope.Gist | Github.TokenScope.Repo; internal BasicCredentialPromptDelegate _basicCredentialPrompt = ConsoleFunctions.CredentialPrompt; @@ -135,6 +140,11 @@ partial class Program { KeyType.Validate, "validate" }, { KeyType.VstsScope,"vstsScope" }, { KeyType.Writelog, "writeLog" }, + { KeyType.KeyVaultUrl, "keyvaultUrl" }, + { KeyType.KeyVaultUseMsi, "keyVaultUseMsi" }, + { KeyType.KeyVaultAuthCertificateStoreType, "keyvaultAuthCertificateStoreType" }, + { KeyType.KeyVaultAuthCertificateThumbprint, "keyvaultAuthCertificateThumbprint" }, + { KeyType.KeyVaultAuthClientId, "keyvaultAuthClientId" }, }; internal readonly Dictionary _environmentKeys = new Dictionary() { diff --git a/azure-pipelines-1.yml b/azure-pipelines-1.yml new file mode 100644 index 000000000..106bb0483 --- /dev/null +++ b/azure-pipelines-1.yml @@ -0,0 +1,38 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + + +pool: + vmImage: 'windows-latest' + +steps: +- task: PowerShell@2 + inputs: + targetType: 'inline' + script: | + Write-Host "Build.BuildUri:" $(Build.BuildUri) + Write-Host "System.CollectionUri:" $(System.CollectionUri) + Write-Host "System.TeamFoundationCollectionUri:" $(System.TeamFoundationCollectionUri) + Write-Host "Build.Repository.Uri:" $(Build.Repository.Uri) + Write-Host "Build Source Directory:" $(Build.SourcesDirectory) +- task: UseDotNet@2 + inputs: + packageType: 'runtime' + version: '3.1.x' + +- task: NuGetCommand@2 + displayName: NuGet restore + inputs: + restoreSolution: 'GitCredentialManager.sln' + verbosityRestore: 'quiet' + +- task: MSBuild@1 + displayName: 'Core Build' + inputs: + solution: "GitCredentialManager.sln" + msbuildArguments: /nologo "/binaryLogger:$(Build.SourcesDirectory)/$(build.buildNumber).binlog" + platform: 'Any CPU' + configuration: 'Debug' + maximumCpuCount: false diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..f7cc46389 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,40 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + + +pool: + vmImage: 'windows-latest' + +steps: + +- task: PowerShell@2 + inputs: + targetType: 'inline' + script: | + Write-Host "Build.BuildUri:" $(Build.BuildUri) + Write-Host "System.CollectionUri:" $(System.CollectionUri) + Write-Host "System.TeamFoundationCollectionUri:" $(System.TeamFoundationCollectionUri) + Write-Host "Build.Repository.Uri:" $(Build.Repository.Uri) + Write-Host "Build Source Directory:" $(Build.SourcesDirectory) + java +- task: UseDotNet@2 + inputs: + packageType: 'runtime' + version: '3.1.x' + +- task: NuGetCommand@2 + displayName: NuGet restore + inputs: + restoreSolution: 'GitCredentialManager.sln' + verbosityRestore: 'quiet' + +- task: MSBuild@1 + displayName: 'Core Build' + inputs: + solution: "GitCredentialManager.sln" + msbuildArguments: /nologo "/binaryLogger:$(Build.SourcesDirectory)/$(build.buildNumber).binlog" + platform: 'Any CPU' + configuration: 'Debug' + maximumCpuCount: false