From c840f4064c0e8daa5509a44c4e4a8d9721c73a44 Mon Sep 17 00:00:00 2001 From: David Federman Date: Tue, 13 Aug 2024 09:15:32 -0700 Subject: [PATCH] AzureBlobStorage: Add interactive and MI auth (#80) --- README.md | 11 +++- .../AzureBlobStoragePluginSettings.cs | 17 ++++++ .../AzureStorageCredentialsType.cs | 28 ++++++++++ .../MSBuildCacheAzureBlobStoragePlugin.cs | 56 ++++++++++++++++--- ...soft.MSBuildCache.AzureBlobStorage.targets | 16 ++++++ src/Common/PluginSettings.cs | 14 +++++ .../SourceControl/GitFileHashProvider.cs | 2 +- 7 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 src/AzureBlobStorage/AzureBlobStoragePluginSettings.cs create mode 100644 src/AzureBlobStorage/AzureStorageCredentialsType.cs diff --git a/README.md b/README.md index 1baba7f..3771612 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,16 @@ This implementation uses [Azure Blob Storage](https://azure.microsoft.com/en-us/ > [!WARNING] > This implementation does not yet have a robust security model. All builds using this will need write access to the storage resource, so for example an external contributor could send a PR which would write/overwrite arbitrary content which could then be used by CI builds. Builds using this plugin must be restricted to trusted team members. Use at your own risk. -The connection string to the blob storage account must be provided in the `MSBUILDCACHE_CONNECTIONSTRING` environment variable. This connection string needs both read and write access to the resource. +These settings are available in addition to the [Common Settings](#common-settings): + +| MSBuild Property Name | Setting Type | Default value | Description | +| ------------- | ------------ | ------------- | ----------- | +| `$(MSBuildCacheCredentialsType)` | `string` | "Interactive" | Indicates the credential type to use for authentication. Valid values are "Interactive", "ConnectionString", "ManagedIdentity" | +| `$(MSBuildCacheBlobUri)` | `Uri` | | Specifies the uri of the Azure Storage Blob. | +| `$(MSBuildCacheManagedIdentityClientId)` | `string` | | Specifies the managed identity client id when using the "ManagedIdentity" credential type | +| `$(MSBuildCacheInteractiveAuthTokenDirectory)` | `string` | "%LOCALAPPDATA%\MSBuildCache\AuthTokenCache" | Specifies a token cache directory when using the "ManagedIdentity" credential type | + +When using the "ConnectionString" credential type, the connection string to the blob storage account must be provided in the `MSBUILDCACHE_CONNECTIONSTRING` environment variable. This connection string needs both read and write access to the resource. ## Other Packages diff --git a/src/AzureBlobStorage/AzureBlobStoragePluginSettings.cs b/src/AzureBlobStorage/AzureBlobStoragePluginSettings.cs new file mode 100644 index 0000000..7c25f2f --- /dev/null +++ b/src/AzureBlobStorage/AzureBlobStoragePluginSettings.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.MSBuildCache.AzureBlobStorage; + +public class AzureBlobStoragePluginSettings : PluginSettings +{ + public AzureStorageCredentialsType CredentialsType { get; init; } = AzureStorageCredentialsType.Interactive; + + public Uri? BlobUri { get; init; } + + public string? ManagedIdentityClientId { get; init; } + + public string InteractiveAuthTokenDirectory { get; init; } = Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\MSBuildCache\AuthTokenCache"); +} diff --git a/src/AzureBlobStorage/AzureStorageCredentialsType.cs b/src/AzureBlobStorage/AzureStorageCredentialsType.cs new file mode 100644 index 0000000..18c1cfc --- /dev/null +++ b/src/AzureBlobStorage/AzureStorageCredentialsType.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.MSBuildCache.AzureBlobStorage; + +/// +/// Determines how to authenticate to Azure Storage. +/// +public enum AzureStorageCredentialsType +{ + /// + /// Use interactive authentication. + /// + Interactive, + + /// + /// Use a connection string to authenticate. + /// + /// + /// The "MSBUILDCACHE_CONNECTIONSTRING" environment variable must contain the connection string to use. + /// + ConnectionString, + + /// + /// Use a managed identity to authenticate. + /// + ManagedIdentity, +} diff --git a/src/AzureBlobStorage/MSBuildCacheAzureBlobStoragePlugin.cs b/src/AzureBlobStorage/MSBuildCacheAzureBlobStoragePlugin.cs index 10fe031..3fbc3ef 100644 --- a/src/AzureBlobStorage/MSBuildCacheAzureBlobStoragePlugin.cs +++ b/src/AzureBlobStorage/MSBuildCacheAzureBlobStoragePlugin.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; @@ -26,7 +25,7 @@ namespace Microsoft.MSBuildCache.AzureBlobStorage; -public sealed class MSBuildCacheAzureBlobStoragePlugin : MSBuildCachePluginBase +public sealed class MSBuildCacheAzureBlobStoragePlugin : MSBuildCachePluginBase { // Note: This is not in PluginSettings as that's configured through item metadata and thus makes it into MSBuild logs. This is a secret so that's not desirable. private const string AzureBlobConnectionStringEnvVar = "MSBUILDCACHE_CONNECTIONSTRING"; @@ -72,8 +71,10 @@ protected override async Task CreateCacheClientAsync(PluginLoggerB logger.LogMessage($"Using cache namespace '{cacheContainer}' as '{cacheContainerHash}'."); + IAzureStorageCredentials credentials = CreateAzureStorageCredentials(Settings, cancellationToken); + #pragma warning disable CA2000 // Dispose objects before losing scope. Expected to be disposed by TwoLevelCache - ICache remoteCache = CreateRemoteCache(new OperationContext(context, cancellationToken), cacheContainerHash, Settings.RemoteCacheIsReadOnly); + ICache remoteCache = CreateRemoteCache(new OperationContext(context, cancellationToken), cacheContainerHash, Settings.RemoteCacheIsReadOnly, credentials); #pragma warning restore CA2000 // Dispose objects before losing scope ICacheSession remoteCacheSession = await StartCacheSessionAsync(context, remoteCache, "remote"); @@ -100,18 +101,55 @@ protected override async Task CreateCacheClientAsync(PluginLoggerB Settings.AsyncCacheMaterialization); } - private static ICache CreateRemoteCache(OperationContext context, string cacheUniverse, bool isReadOnly) + private static IAzureStorageCredentials CreateAzureStorageCredentials(AzureBlobStoragePluginSettings settings, CancellationToken cancellationToken) { - string? connectionString = Environment.GetEnvironmentVariable(AzureBlobConnectionStringEnvVar); - if (string.IsNullOrEmpty(connectionString)) + switch (settings.CredentialsType) { - throw new InvalidOperationException($"Required environment variable '{AzureBlobConnectionStringEnvVar}' not set"); + case AzureStorageCredentialsType.Interactive: + { + if (settings.BlobUri is null) + { + throw new InvalidOperationException($"{nameof(AzureBlobStoragePluginSettings.BlobUri)} is required when using {nameof(AzureBlobStoragePluginSettings.CredentialsType)}={settings.CredentialsType}"); + } + + return new InteractiveClientStorageCredentials(settings.InteractiveAuthTokenDirectory, settings.BlobUri, cancellationToken); + } + case AzureStorageCredentialsType.ConnectionString: + { + string? connectionString = Environment.GetEnvironmentVariable(AzureBlobConnectionStringEnvVar); + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException($"Required environment variable '{AzureBlobConnectionStringEnvVar}' not set"); + } + + return new SecretBasedAzureStorageCredentials(connectionString); + } + case AzureStorageCredentialsType.ManagedIdentity: + { + if (settings.BlobUri is null) + { + throw new InvalidOperationException($"{nameof(AzureBlobStoragePluginSettings.BlobUri)} is required when using {nameof(AzureBlobStoragePluginSettings.CredentialsType)}={settings.CredentialsType}"); + } + + if (string.IsNullOrEmpty(settings.ManagedIdentityClientId)) + { + throw new InvalidOperationException($"{nameof(AzureBlobStoragePluginSettings.BlobUri)} is required when using {nameof(AzureBlobStoragePluginSettings.CredentialsType)}={settings.CredentialsType}"); + } + + return new ManagedIdentityAzureStorageCredentials(settings.ManagedIdentityClientId!, settings.BlobUri); + } + default: + { + throw new InvalidOperationException($"Unknown {nameof(AzureBlobStoragePluginSettings.CredentialsType)}: {settings.CredentialsType}"); + } } + } - SecretBasedAzureStorageCredentials credentials = new(connectionString); + private static ICache CreateRemoteCache(OperationContext context, string cacheUniverse, bool isReadOnly, IAzureStorageCredentials credentials) + { BlobCacheStorageAccountName accountName = BlobCacheStorageAccountName.Parse(credentials.GetAccountName()); AzureBlobStorageCacheFactory.Configuration cacheConfig = new( - ShardingScheme: new ShardingScheme(ShardingAlgorithm.SingleShard, new List { accountName }), + ShardingScheme: new ShardingScheme(ShardingAlgorithm.SingleShard, [accountName]), Universe: cacheUniverse, Namespace: "0", RetentionPolicyInDays: null, diff --git a/src/AzureBlobStorage/build/Microsoft.MSBuildCache.AzureBlobStorage.targets b/src/AzureBlobStorage/build/Microsoft.MSBuildCache.AzureBlobStorage.targets index 660dd7d..d616193 100644 --- a/src/AzureBlobStorage/build/Microsoft.MSBuildCache.AzureBlobStorage.targets +++ b/src/AzureBlobStorage/build/Microsoft.MSBuildCache.AzureBlobStorage.targets @@ -1,3 +1,19 @@ + + $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheCredentialsType + $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheBlobUri + $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheManagedIdentityClientId + $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheInteractiveAuthTokenDirectory + + + + + + $(MSBuildCacheCredentialsType) + $(MSBuildCacheBlobUri) + $(MSBuildCacheManagedIdentityClientId) + $(MSBuildCacheInteractiveAuthTokenDirectory) + + \ No newline at end of file diff --git a/src/Common/PluginSettings.cs b/src/Common/PluginSettings.cs index 54fe4b4..fed4b7b 100644 --- a/src/Common/PluginSettings.cs +++ b/src/Common/PluginSettings.cs @@ -243,6 +243,20 @@ private static SettingParseResult TryParseSettingValue( return settingValue == null ? SettingParseResult.InvalidValue : SettingParseResult.Success; } + if (type == typeof(Uri)) + { + if (Uri.TryCreate(rawSettingValue, UriKind.Absolute, out Uri? uri)) + { + settingValue = uri; + return SettingParseResult.Success; + } + else + { + settingValue = null; + return SettingParseResult.InvalidValue; + } + } + if (type == typeof(Glob)) { string globSpec = rawSettingValue; diff --git a/src/Common/SourceControl/GitFileHashProvider.cs b/src/Common/SourceControl/GitFileHashProvider.cs index fd8e7e6..1f6f641 100644 --- a/src/Common/SourceControl/GitFileHashProvider.cs +++ b/src/Common/SourceControl/GitFileHashProvider.cs @@ -60,7 +60,7 @@ public async Task> GetFileHashesAsync(string } // Iterate through the initialized submodules and add those hashes - IList submodules = await GetInitializedSubmodulesAsync(repoRoot, cancellationToken); + List submodules = await GetInitializedSubmodulesAsync(repoRoot, cancellationToken); if (submodules.Count == 0) {