Skip to content

Commit

Permalink
Azure DP Initializer (#14750)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtkech authored Nov 26, 2023
1 parent 425b7f3 commit 8fb1627
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace OrchardCore.DataProtection.Azure;

public class BlobOptions
{
public string ConnectionString { get; set; }

public string ContainerName { get; set; } = "dataprotection";

public string BlobName { get; set; }

public bool CreateContainer { get; set; } = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Fluid;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;

namespace OrchardCore.DataProtection.Azure;

public class BlobOptionsSetup
{
private readonly FluidParser _fluidParser = new();

private readonly IShellConfiguration _configuration;
private readonly ShellOptions _shellOptions;
private readonly ShellSettings _shellSettings;
private readonly ILogger _logger;

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

public async Task ConfigureAsync(BlobOptions options)
{
_configuration.Bind("OrchardCore_DataProtection_Azure", options);
await ConfigureContainerNameAsync(options);
await ConfigureBlobNameAsync(options);
}

private async Task ConfigureContainerNameAsync(BlobOptions options)
{
try
{
// Use Fluid directly as the service provider has not been built.
var templateOptions = new TemplateOptions();
templateOptions.MemberAccessStrategy.Register<ShellSettings>();
var templateContext = new TemplateContext(templateOptions);
templateContext.SetValue("ShellSettings", _shellSettings);

var template = _fluidParser.Parse(options.ContainerName);

// Container name must be lowercase.
var containerName = template.Render(templateContext, NullEncoder.Default).ToLower();
options.ContainerName = containerName.Replace("\r", string.Empty).Replace("\n", string.Empty);
}
catch (Exception e)
{
_logger.LogCritical(e, "Unable to parse data protection connection string.");
throw;
}

if (options.CreateContainer)
{
try
{
_logger.LogDebug("Testing data protection container {ContainerName} existence", options.ContainerName);
var 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`.");
throw;
}
}
}

private async Task ConfigureBlobNameAsync(BlobOptions options)
{
if (string.IsNullOrEmpty(options.BlobName))
{
options.BlobName = $"{_shellOptions.ShellsContainerName}/{_shellSettings.Name}/DataProtectionKeys.xml";

return;
}

try
{
// Use Fluid directly as the service provider has not been built.
var templateOptions = new TemplateOptions();
var templateContext = new TemplateContext(templateOptions);
templateOptions.MemberAccessStrategy.Register<ShellSettings>();
templateContext.SetValue("ShellSettings", _shellSettings);

var template = _fluidParser.Parse(options.BlobName);

var blobName = await template.RenderAsync(templateContext, NullEncoder.Default);
options.BlobName = blobName.Replace("\r", string.Empty).Replace("\n", string.Empty);
}
catch (Exception e)
{
_logger.LogCritical(e, "Unable to parse data protection blob name.");
throw;
}
}
}
151 changes: 34 additions & 117 deletions src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,139 +1,56 @@
using System;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Fluid;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Modules;

namespace OrchardCore.DataProtection.Azure
namespace OrchardCore.DataProtection.Azure;

public class Startup : StartupBase
{
public class Startup : StartupBase
private readonly IShellConfiguration _configuration;
private readonly ILogger _logger;

public Startup(IShellConfiguration configuration, ILogger<Startup> logger)
{
private readonly ShellOptions _shellOptions;
private readonly ShellSettings _shellSettings;
private readonly ILogger _logger;
_configuration = configuration;
_logger = logger;
}

// Local instance since it can be discarded once the startup is over.
private readonly FluidParser _fluidParser = new();
// Assume that this module will override default configuration, so set the Order to a value above the default.
public override int Order => 10;

public Startup(
IOptions<ShellOptions> shellOptions,
ShellSettings shellSettings,
ILogger<Startup> logger)
{
_shellOptions = shellOptions.Value;
_shellSettings = shellSettings;
_logger = logger;
}
public override void ConfigureServices(IServiceCollection services)
{
var connectionString = _configuration.GetValue<string>("OrchardCore_DataProtection_Azure:ConnectionString");

public override void ConfigureServices(IServiceCollection services)
if (!string.IsNullOrWhiteSpace(connectionString))
{
services.Initialize(async sp =>
{
var configuration = sp.GetRequiredService<IShellConfiguration>();

var connectionString = configuration.GetValue<string>("OrchardCore_DataProtection_Azure:ConnectionString");

if (!string.IsNullOrWhiteSpace(connectionString))
services
.AddSingleton(new BlobOptions())
.AddTransient<BlobOptionsSetup>()
.AddDataProtection()
.PersistKeysToAzureBlobStorage(sp =>
{
var containerName = await GetBlobContainerNameAsync(configuration, connectionString);

services.AddDataProtection()
.PersistKeysToAzureBlobStorage(connectionString, containerName, await GetBlobNameAsync(configuration));
}
else
{
_logger.LogCritical("No connection string was supplied for OrchardCore.DataProtection.Azure. Ensure that an application setting containing a valid Azure Storage connection string is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`.");
}
});
}

private async Task<string> GetBlobContainerNameAsync(IShellConfiguration configuration, string connectionString)
{
var containerName = configuration.GetValue("OrchardCore_DataProtection_Azure:ContainerName", "dataprotection");

// Use Fluid directly as the service provider has not been built.
try
{
var templateOptions = new TemplateOptions();
templateOptions.MemberAccessStrategy.Register<ShellSettings>();
var templateContext = new TemplateContext(templateOptions);
templateContext.SetValue("ShellSettings", _shellSettings);

var template = _fluidParser.Parse(containerName);
var options = sp.GetRequiredService<BlobOptions>();
return new BlobClient(
options.ConnectionString,
options.ContainerName,
options.BlobName);
});

// container name must be lowercase
containerName = template.Render(templateContext, NullEncoder.Default).ToLower();
containerName = containerName.Replace("\r", string.Empty).Replace("\n", string.Empty);
}
catch (Exception e)
{
_logger.LogCritical(e, "Unable to parse data protection connection string.");
throw;
}

var createContainer = configuration.GetValue("OrchardCore_DataProtection_Azure:CreateContainer", true);
if (createContainer)
services.Initialize(async sp =>
{
try
{
_logger.LogDebug("Testing data protection container {ContainerName} existence", containerName);
var _blobContainer = new BlobContainerClient(connectionString, containerName);
var response = await _blobContainer.CreateIfNotExistsAsync(PublicAccessType.None);
_logger.LogDebug("Data protection container {ContainerName} created.", containerName);
}
catch (Exception)
{
_logger.LogCritical("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`.");

throw;
}
}

return containerName;
var options = sp.GetRequiredService<BlobOptions>();
var setup = sp.GetRequiredService<BlobOptionsSetup>();
await setup.ConfigureAsync(options);
});
}

private async Task<string> GetBlobNameAsync(IShellConfiguration configuration)
else
{
var blobName = configuration.GetValue<string>("OrchardCore_DataProtection_Azure:BlobName");

if (string.IsNullOrEmpty(blobName))
{
blobName = $"{_shellOptions.ShellsContainerName}/{_shellSettings.Name}/DataProtectionKeys.xml";
}
else
{
try
{
// Use Fluid directly as the service provider has not been built.
var templateOptions = new TemplateOptions();
var templateContext = new TemplateContext(templateOptions);
templateOptions.MemberAccessStrategy.Register<ShellSettings>();
templateContext.SetValue("ShellSettings", _shellSettings);

var template = _fluidParser.Parse(blobName);

blobName = await template.RenderAsync(templateContext, NullEncoder.Default);
blobName = blobName.Replace("\r", string.Empty).Replace("\n", string.Empty);
}
catch (Exception e)
{
_logger.LogCritical(e, "Unable to parse data protection blob name.");
throw;
}
}

return blobName;
_logger.LogCritical("No connection string was supplied for OrchardCore.DataProtection.Azure. Ensure that an application setting containing a valid Azure Storage connection string is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`.");
}

// Assume that this module will override default configuration, so set the Order to a value above the default.
public override int Order => 10;
}
}

0 comments on commit 8fb1627

Please sign in to comment.