Setting Umbraco to work with Azure private storage

Umbraco media and Azure private blob storage insights

Setting up Umbraco to work with Azure blobs is quite easy with UmbracoFileSystemProviders.Azure package when the blob storage has default settings and "Allow blob public access" is enabled.

However, if you want to follow MS security recommendations and store media files on private blob storage - things get a bit more complicated and you need to take few additional steps.

Azure storage configuration - Allow Blob public access disabled

Should I use private or public access? 

When a container is configured for public access, any client can read data in that container. Public access presents a potential security risk, so if your scenario does not require it, Microsoft recommends that you disallow it for the storage account

by Microsoft

You should follow the great thread here to get some good insights regarding common problems and suggestions on working with Umbraco and the Private Azure blob container.

As James Jackson-South suggested:

You'd need to create your own IImageService implementation since CloudImageService is essentially a HttpRequest

Once you will go through the whole discussion, you will notice that you need to take two additional steps to make things work:

  • Implement custom IImageService
  • Update the security.config file

Specification of the Umbraco environment and libraries

  • ImageProcessor 2.7.0.100
  • ImageProcessor.Web 4.10.0.100
  • ImageProcessor.Web.Plugins.AzureBlobCache 1.5.0.100
  • WindowsAzure.Storage 8.7.0
  • UmbracoFileSystemProviders.Azure 2.0.0-alpha1
  • UmbracoCms 8.6.0
  • .NET Framework version: 4.7.2

ImageProcessor's IImageService custom implementation

The core part of the service is GetImage(object id) method which is responsible for getting the media from the Azure blob storage. Note, that the blob name (id) should not include the container as a prefix and should look as follows: "3gikqq22/example-image.png" format.

/// <summary>
/// An image service for retrieving images from Azure.
/// </summary>
public class AzureImageService : IImageService
{
   private CloudBlobContainer _blobContainer;
   private CloudStorageAccount _storageAccount;
   private Dictionary<string, string> _settings = new Dictionary<string, string>();

   /// <summary>
   /// Gets or sets the prefix for the given implementation.
   /// <remarks>
   /// This value is used as a prefix for any image requests that should use this service.
   /// </remarks>
   /// </summary>
   public string Prefix { get; set; } = string.Empty;

   /// <summary>
   /// Gets a value indicating whether the image service requests files from
   /// the locally based file system.
   /// </summary>
   public bool IsFileLocalService => false;

   /// <summary>
   /// Gets or sets any additional settings required by the service.
   /// </summary>
   public Dictionary<string, string> Settings
   {
      get => this._settings;
      set
      {
         this._settings = value;
         this.InitService();
      }
   }

   /// <summary>
   /// Gets or sets the white list of <see cref="Uri" />. 
   /// </summary>
   public Uri[] WhiteList { get; set; }

   /// <summary>
   /// Gets the image using the given identifier.
   /// </summary>
   /// <param name="id"></param>
   /// <returns></returns>
   public async Task<byte[]> GetImage(object id)
   {
      if (await _blobContainer.ExistsAsync())
      {
         //expecting id as "3gikqq22/example-image.png"
         string sId = PrepareBlobName(id);

         CloudBlockBlob blob = _blobContainer.GetBlockBlobReference(sId);

         if (await blob.ExistsAsync())
         {
            using (MemoryStream memoryStream = MemoryStreamPool.Shared.GetStream())
            {
               await blob.DownloadToStreamAsync(memoryStream).ConfigureAwait(false);
               return memoryStream.ToArray();
            }
         }
      }

      return null;
   }

   /// <summary>
   /// Removes container prefix from blob path
   /// </summary>
   /// <param name="id"></param>
   /// <returns></returns>
   private string PrepareBlobName(object id)
   {
      string sId = id.ToString();

      if (sId.StartsWith($"/{this.Settings["Container"]}/"))
      {
         return sId.Substring(this.Settings["Container"].Length + 2);
      }

      return sId;
   }


   /// <summary>
   /// Gets a value indicating whether the current request passes sanitizing rules.
   /// </summary>
   /// <param name="path">The image path.</param>
   /// <returns>
   /// <c>True</c> if the request is valid; otherwise, <c>False</c>.
   /// </returns>
   public bool IsValidRequest(string path) => ImageHelpers.IsValidImageExtension(path);

   /// <summary>
   /// Initialise the service.
   /// </summary>
   private void InitService()
   {
      // Retrieve storage accounts from connection string.
      _storageAccount = CloudStorageAccount.Parse(this.Settings["StorageAccount"]);

      // Create the blob client.
      CloudBlobClient blobClient = _storageAccount.CreateCloudBlobClient();

      string container = this.Settings.ContainsKey("Container")
         ? this.Settings["Container"]
         : string.Empty;

      BlobContainerPublicAccessType accessType = this.Settings.ContainsKey("AccessType")
         ? (BlobContainerPublicAccessType)Enum.Parse(typeof(BlobContainerPublicAccessType), this.Settings["AccessType"])
         : BlobContainerPublicAccessType.Blob;

      this._blobContainer = CreateContainer(blobClient, container, accessType);
   }

   /// <summary>
   /// Returns the cache container, creating a new one if none exists.
   /// </summary>
   /// <param name="cloudBlobClient"><see cref="CloudBlobClient"/> where the container is stored.</param>
   /// <param name="containerName">The name of the container.</param>
   /// <param name="accessType"><see cref="BlobContainerPublicAccessType"/> indicating the access permissions.</param>
   /// <returns>The <see cref="CloudBlobContainer"/></returns>
   private static CloudBlobContainer CreateContainer(CloudBlobClient cloudBlobClient, string containerName, BlobContainerPublicAccessType accessType)
   {
      CloudBlobContainer container = cloudBlobClient.GetContainerReference(containerName);

      if (!container.Exists())
      {
         container.Create();
         container.SetPermissions(new BlobContainerPermissions { PublicAccess = accessType });
      }

      return container;
   }
}
To download the source code for this post, please visit GitHub

Updating security.config file

Once the new service is ready, you should find ~\config\imageprocessor\security.config file and define new service tag as follows: 

<?xml version="1.0" encoding="utf-8"?>
<security>
	<services>		
		<service name="AzureImageService" type="[Namespace].AzureImageService, [Namespace]">
			<settings>
				<setting key="StorageAccount" value="[StorageAccountConnectionString]" />
				<setting key="Container" value="media" />
				<setting key="AccessType" value="Off" />
			</settings>
		</service>
	</services>
</security>

Now ImageProcessor will use the new implementation to get the media files.

Comments
Leave a Comment