Skip to content

Commit

Permalink
AzureBlobStorage: Add interactive and MI auth (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfederm authored Aug 13, 2024
1 parent f05fd79 commit c840f40
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 11 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions src/AzureBlobStorage/AzureBlobStoragePluginSettings.cs
Original file line number Diff line number Diff line change
@@ -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");
}
28 changes: 28 additions & 0 deletions src/AzureBlobStorage/AzureStorageCredentialsType.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Determines how to authenticate to Azure Storage.
/// </summary>
public enum AzureStorageCredentialsType
{
/// <summary>
/// Use interactive authentication.
/// </summary>
Interactive,

/// <summary>
/// Use a connection string to authenticate.
/// </summary>
/// <remarks>
/// The "MSBUILDCACHE_CONNECTIONSTRING" environment variable must contain the connection string to use.
/// </remarks>
ConnectionString,

/// <summary>
/// Use a managed identity to authenticate.
/// </summary>
ManagedIdentity,
}
56 changes: 47 additions & 9 deletions src/AzureBlobStorage/MSBuildCacheAzureBlobStoragePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,7 +25,7 @@

namespace Microsoft.MSBuildCache.AzureBlobStorage;

public sealed class MSBuildCacheAzureBlobStoragePlugin : MSBuildCachePluginBase
public sealed class MSBuildCacheAzureBlobStoragePlugin : MSBuildCachePluginBase<AzureBlobStoragePluginSettings>
{
// 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";
Expand Down Expand Up @@ -72,8 +71,10 @@ protected override async Task<ICacheClient> 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");
Expand All @@ -100,18 +101,55 @@ protected override async Task<ICacheClient> 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<BlobCacheStorageAccountName> { accountName }),
ShardingScheme: new ShardingScheme(ShardingAlgorithm.SingleShard, [accountName]),
Universe: cacheUniverse,
Namespace: "0",
RetentionPolicyInDays: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
<Project>
<PropertyGroup Condition="'$(MSBuildCacheEnabled)' != 'false'">
<MSBuildCacheGlobalPropertiesToIgnore>$(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheCredentialsType</MSBuildCacheGlobalPropertiesToIgnore>
<MSBuildCacheGlobalPropertiesToIgnore>$(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheBlobUri</MSBuildCacheGlobalPropertiesToIgnore>
<MSBuildCacheGlobalPropertiesToIgnore>$(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheManagedIdentityClientId</MSBuildCacheGlobalPropertiesToIgnore>
<MSBuildCacheGlobalPropertiesToIgnore>$(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheInteractiveAuthTokenDirectory</MSBuildCacheGlobalPropertiesToIgnore>
</PropertyGroup>

<Import Project="Microsoft.MSBuildCache.Common.targets" />

<ItemGroup Condition="'$(MSBuildCacheEnabled)' != 'false'">
<ProjectCachePlugin Update="$(MSBuildCacheAssembly)">
<CredentialsType>$(MSBuildCacheCredentialsType)</CredentialsType>
<BlobUri>$(MSBuildCacheBlobUri)</BlobUri>
<ManagedIdentityClientId>$(MSBuildCacheManagedIdentityClientId)</ManagedIdentityClientId>
<InteractiveAuthTokenDirectory>$(MSBuildCacheInteractiveAuthTokenDirectory)</InteractiveAuthTokenDirectory>
</ProjectCachePlugin>
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions src/Common/PluginSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Common/SourceControl/GitFileHashProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task<IReadOnlyDictionary<string, byte[]>> GetFileHashesAsync(string
}

// Iterate through the initialized submodules and add those hashes
IList<string> submodules = await GetInitializedSubmodulesAsync(repoRoot, cancellationToken);
List<string> submodules = await GetInitializedSubmodulesAsync(repoRoot, cancellationToken);

if (submodules.Count == 0)
{
Expand Down

0 comments on commit c840f40

Please sign in to comment.