Skip to content

Commit

Permalink
feat: Support azure blob storage auth with managed identity.
Browse files Browse the repository at this point in the history
  • Loading branch information
mariojsnunes committed Apr 16, 2024
1 parent f59edd1 commit b6c2a04
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 26 deletions.
Expand Up @@ -11,4 +11,6 @@ public class BlobOptions : IAsyncOptions
public string BlobName { get; set; }

public bool CreateContainer { get; set; } = true;

public string AzureClientName { get; set; }
}
Expand Up @@ -3,6 +3,7 @@
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Fluid;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -18,17 +19,20 @@ public class BlobOptionsSetup : IAsyncConfigureOptions<BlobOptions>
private readonly IShellConfiguration _configuration;
private readonly ShellOptions _shellOptions;
private readonly ShellSettings _shellSettings;
private readonly IAzureClientFactory<BlobServiceClient> _azureClientFactory;
private readonly ILogger _logger;

public BlobOptionsSetup(
IShellConfiguration configuration,
IOptions<ShellOptions> shellOptions,
ShellSettings shellSettings,
IAzureClientFactory<BlobServiceClient> azureClientFactory,
ILogger<BlobOptionsSetup> logger)
{
_configuration = configuration;
_shellOptions = shellOptions.Value;
_shellSettings = shellSettings;
_azureClientFactory = azureClientFactory;
_logger = logger;
}

Expand Down Expand Up @@ -66,13 +70,24 @@ private async ValueTask ConfigureContainerNameAsync(BlobOptions options)
try
{
_logger.LogDebug("Testing data protection container {ContainerName} existence", options.ContainerName);
var blobContainer = new BlobContainerClient(options.ConnectionString, options.ContainerName);

BlobContainerClient blobContainer;

if (!string.IsNullOrWhiteSpace(options.AzureClientName))
{
blobContainer = _azureClientFactory.CreateClient(options.AzureClientName).GetBlobContainerClient(options.ContainerName);
}
else
{
blobContainer = new BlobContainerClient(options.ConnectionString, options.ContainerName);
}

var response = await blobContainer.CreateIfNotExistsAsync(PublicAccessType.None);
_logger.LogDebug("Data protection container {ContainerName} created.", options.ContainerName);
}
catch (Exception e)
{
_logger.LogCritical(e, "Unable to connect to Azure Storage to configure data protection storage. Ensure that an application setting containing a valid Azure Storage connection string is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`.");
_logger.LogCritical(e, "Unable to connect to Azure Storage to configure data protection storage. Ensure that an application setting containing a valid Azure Storage ConnectionString or AzureClientName is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`.");
throw;
}
}
Expand Down
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- NuGet properties-->
Expand All @@ -15,6 +15,7 @@

<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" />
<PackageReference Include="Microsoft.Extensions.Azure" />
</ItemGroup>

<ItemGroup>
Expand Down
@@ -1,5 +1,6 @@
using Azure.Storage.Blobs;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -34,6 +35,14 @@ public override void ConfigureServices(IServiceCollection services)
.PersistKeysToAzureBlobStorage(sp =>
{
var options = sp.GetRequiredService<BlobOptions>();
if (!string.IsNullOrWhiteSpace(options.AzureClientName))
{
var azureClientFactory = sp.GetRequiredService<IAzureClientFactory<BlobServiceClient>>();
return azureClientFactory.CreateClient(options.AzureClientName).GetBlobContainerClient(options.ContainerName).GetBlobClient(options.BlobName);
}
return new BlobClient(
options.ConnectionString,
options.ContainerName,
Expand Down
@@ -1,12 +1,12 @@
using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Removing;
using OrchardCore.FileStorage.AzureBlob;
using OrchardCore.Modules;

namespace OrchardCore.Media.Azure
Expand All @@ -16,27 +16,30 @@ public class MediaBlobContainerTenantEvents : ModularTenantEvents
private readonly MediaBlobStorageOptions _options;
private readonly ShellSettings _shellSettings;
protected readonly IStringLocalizer S;
private readonly BlobContainerClientFactory _blobContainerClientFactory;
private readonly ILogger _logger;

public MediaBlobContainerTenantEvents(
IOptions<MediaBlobStorageOptions> options,
ShellSettings shellSettings,
IStringLocalizer<MediaBlobContainerTenantEvents> localizer,
BlobContainerClientFactory blobContainerClientFactory,
ILogger<MediaBlobContainerTenantEvents> logger
)
{
_options = options.Value;
_shellSettings = shellSettings;
S = localizer;
_blobContainerClientFactory = blobContainerClientFactory;
_logger = logger;
}

public override async Task ActivatingAsync()
{
// Only create container if options are valid.
if (_shellSettings.IsUninitialized() ||
string.IsNullOrEmpty(_options.ConnectionString) ||
string.IsNullOrEmpty(_options.ContainerName) ||
(string.IsNullOrWhiteSpace(_options.ConnectionString) && string.IsNullOrWhiteSpace(_options.AzureClientName)) ||
string.IsNullOrWhiteSpace(_options.ContainerName) ||
!_options.CreateContainer
)
{
Expand All @@ -47,7 +50,7 @@ public override async Task ActivatingAsync()

try
{
var _blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
var _blobContainer = _blobContainerClientFactory.Create(_options);
var response = await _blobContainer.CreateIfNotExistsAsync(PublicAccessType.None);

_logger.LogDebug("Azure Media Storage container {ContainerName} created.", _options.ContainerName);
Expand All @@ -62,15 +65,15 @@ public override async Task RemovingAsync(ShellRemovingContext context)
{
// Only remove container if options are valid.
if (!_options.RemoveContainer ||
string.IsNullOrEmpty(_options.ConnectionString) ||
string.IsNullOrEmpty(_options.ContainerName))
(string.IsNullOrWhiteSpace(_options.ConnectionString) && string.IsNullOrWhiteSpace(_options.AzureClientName)) ||
string.IsNullOrWhiteSpace(_options.ContainerName))
{
return;
}

try
{
var _blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
var _blobContainer = _blobContainerClientFactory.Create(_options);

var response = await _blobContainer.DeleteIfExistsAsync();
if (!response.Value)
Expand Down
Expand Up @@ -37,6 +37,7 @@ public void Configure(MediaBlobStorageOptions options)
options.ConnectionString = section.GetValue(nameof(options.ConnectionString), string.Empty);
options.CreateContainer = section.GetValue(nameof(options.CreateContainer), true);
options.RemoveContainer = section.GetValue(nameof(options.RemoveContainer), false);
options.AzureClientName = section.GetValue(nameof(options.AzureClientName), string.Empty);

var templateOptions = new TemplateOptions();
var templateContext = new TemplateContext(templateOptions);
Expand Down
23 changes: 13 additions & 10 deletions src/OrchardCore.Modules/OrchardCore.Media.Azure/Startup.cs
Expand Up @@ -40,12 +40,10 @@ public override void ConfigureServices(IServiceCollection services)
services.AddScoped<INavigationProvider, AdminMenu>();
services.AddTransient<IConfigureOptions<MediaBlobStorageOptions>, MediaBlobStorageOptionsConfiguration>();

// Only replace default implementation if options are valid.
var connectionString = _configuration[$"OrchardCore_Media_Azure:{nameof(MediaBlobStorageOptions.ConnectionString)}"];
var containerName = _configuration[$"OrchardCore_Media_Azure:{nameof(MediaBlobStorageOptions.ContainerName)}"];

if (CheckOptions(connectionString, containerName, _logger))
if (CheckOptions(_configuration, _logger))
{
services.TryAddSingleton<BlobContainerClientFactory>();

// Register a media cache file provider.
services.AddSingleton<IMediaFileStoreCacheFileProvider>(serviceProvider =>
{
Expand All @@ -57,7 +55,6 @@ public override void ConfigureServices(IServiceCollection services)
}
var mediaOptions = serviceProvider.GetRequiredService<IOptions<MediaOptions>>().Value;
var shellOptions = serviceProvider.GetRequiredService<IOptions<ShellOptions>>();
var shellSettings = serviceProvider.GetRequiredService<ShellSettings>();
var logger = serviceProvider.GetRequiredService<ILogger<DefaultMediaFileStoreCacheFileProvider>>();
Expand Down Expand Up @@ -91,9 +88,10 @@ public override void ConfigureServices(IServiceCollection services)
var contentTypeProvider = serviceProvider.GetRequiredService<IContentTypeProvider>();
var mediaEventHandlers = serviceProvider.GetServices<IMediaEventHandler>();
var mediaCreatingEventHandlers = serviceProvider.GetServices<IMediaCreatingEventHandler>();
var blobContainerClientFactory = serviceProvider.GetRequiredService<BlobContainerClientFactory>();
var logger = serviceProvider.GetRequiredService<ILogger<DefaultMediaFileStore>>();
var fileStore = new BlobFileStore(blobStorageOptions, clock, contentTypeProvider);
var fileStore = new BlobFileStore(blobStorageOptions, blobContainerClientFactory, clock, contentTypeProvider);
var mediaUrlBase = "/" + fileStore.Combine(shellSettings.RequestUrlPrefix, mediaOptions.AssetsRequestPath);
var originalPathBase = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext
Expand All @@ -117,13 +115,18 @@ public override void ConfigureServices(IServiceCollection services)
private static string GetMediaCachePath(IWebHostEnvironment hostingEnvironment, ShellSettings shellSettings, string assetsPath)
=> PathExtensions.Combine(hostingEnvironment.WebRootPath, shellSettings.Name, assetsPath);

private static bool CheckOptions(string connectionString, string containerName, ILogger logger)
private static bool CheckOptions(IShellConfiguration configuration, ILogger logger)
{
var optionsAreValid = true;

if (string.IsNullOrWhiteSpace(connectionString))
// Only replace default implementation if options are valid.
var connectionString = configuration[$"OrchardCore_Media_Azure:{nameof(MediaBlobStorageOptions.ConnectionString)}"];
var containerName = configuration[$"OrchardCore_Media_Azure:{nameof(MediaBlobStorageOptions.ContainerName)}"];
var azureClientName = configuration[$"OrchardCore_Media_Azure:{nameof(MediaBlobStorageOptions.AzureClientName)}"];

if (string.IsNullOrWhiteSpace(azureClientName) && string.IsNullOrWhiteSpace(connectionString))
{
logger.LogError("Azure Media Storage is enabled but not active because the 'ConnectionString' is missing or empty in application configuration.");
logger.LogError("Azure Media Storage is enabled but not active because either 'ConnectionString' or 'AzureClientName' must be set in application configuration.");
optionsAreValid = false;
}

Expand Down
@@ -0,0 +1,26 @@
using Azure.Storage.Blobs;
using Microsoft.Extensions.Azure;

namespace OrchardCore.FileStorage.AzureBlob;

public class BlobContainerClientFactory
{
private readonly IAzureClientFactory<BlobServiceClient> _azureClientFactory;

public BlobContainerClientFactory(IAzureClientFactory<BlobServiceClient> azureClientFactory)
{
_azureClientFactory = azureClientFactory;
}

public BlobContainerClient Create(BlobStorageOptions options)
{
if (!string.IsNullOrWhiteSpace(options.AzureClientName))
{
return _azureClientFactory.CreateClient(options.AzureClientName).GetBlobContainerClient(options.ContainerName);
}
else
{
return new BlobContainerClient(options.ConnectionString, options.ContainerName);
}
}
}
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs;
Expand Down Expand Up @@ -46,13 +45,12 @@ public class BlobFileStore : IFileStore
private readonly IContentTypeProvider _contentTypeProvider;
private readonly string _basePrefix = null;

public BlobFileStore(BlobStorageOptions options, IClock clock, IContentTypeProvider contentTypeProvider)
public BlobFileStore(BlobStorageOptions options, BlobContainerClientFactory blobContainerClientFactory, IClock clock, IContentTypeProvider contentTypeProvider)
{
_options = options;
_clock = clock;
_contentTypeProvider = contentTypeProvider;

_blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
_blobContainer = blobContainerClientFactory.Create(_options);

if (!string.IsNullOrEmpty(_options.BasePath))
{
Expand Down
Expand Up @@ -16,5 +16,11 @@ public abstract class BlobStorageOptions
/// The base directory path to use inside the container for this stores contents.
/// </summary>
public string BasePath { get; set; }

/// <summary>
/// The Azure Client name. Must be configured by AddAzureClients on Startup.
/// https://learn.microsoft.com/en-us/dotnet/azure/sdk/dependency-injection?tabs=web-app-builder#configure-multiple-service-clients-with-different-names
/// </summary>
public string AzureClientName { get; set; }
}
}
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

Expand All @@ -16,6 +16,7 @@

<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" />
<PackageReference Include="Microsoft.Extensions.Azure" />
</ItemGroup>

<ItemGroup>
Expand Down
Expand Up @@ -23,6 +23,7 @@ public static OrchardCoreBuilder AddAzureShellsConfiguration(this OrchardCoreBui
var services = builder.ApplicationServices;

services.TryAddSingleton<IContentTypeProvider, FileExtensionContentTypeProvider>();
services.TryAddSingleton<BlobContainerClientFactory>();

services.AddSingleton<IShellsFileStore>(sp =>
{
Expand All @@ -34,8 +35,9 @@ public static OrchardCoreBuilder AddAzureShellsConfiguration(this OrchardCoreBui
var clock = sp.GetRequiredService<IClock>();
var contentTypeProvider = sp.GetRequiredService<IContentTypeProvider>();
var blobContainerClientFactory = sp.GetRequiredService<BlobContainerClientFactory>();
var fileStore = new BlobFileStore(blobOptions, clock, contentTypeProvider);
var fileStore = new BlobFileStore(blobOptions, blobContainerClientFactory, clock, contentTypeProvider);
return new BlobShellsFileStore(fileStore);
});
Expand Down

0 comments on commit b6c2a04

Please sign in to comment.