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)
{