OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.44k stars 2.4k forks source link

Ability to connect to Azure Blob storage with AAD authentication/managed identity #12639

Open lassiko opened 2 years ago

lassiko commented 2 years ago

Currently, one has to configure Azure storage with connection string containing the AccountKey, which implies the need to expose it in appsettings.json or Azure App Service app settings.

To enhance security, it would be good to be able to use only the storage URL and use AAD authentication and App service's managed identity, as one can already do in OC with Azure SQL database.

sebastienros commented 2 years ago

Would you be able to provide a PR for that?

carlin-q-scott commented 1 year ago

Just some notes for whoever implements this:

  1. Add ContainerUri to OrchardCore_Media_Azure options
  2. Construct BlobContainerClass using this method:
    new BlobServiceClient(
            new Uri(blobOptions.ContainerUri),
            new DefaultAzureCredential())

That's it AFAIK based on the documentation.

rikbosch commented 1 year ago

Just some notes for whoever implements this:

  1. Add ContainerUri to OrchardCore_Media_Azure options
  2. Construct BlobContainerClass using this method:
    new BlobServiceClient(
           new Uri(blobOptions.ContainerUri),
           new DefaultAzureCredential())

That's it AFAIK based on the documentation.

What about configuring BlobClientOptions like retries etc?

Maybe we should be able to specify the 'name' of the blobclient to use and use that in combination with Microsoft.Extensions.Azure :

// first configure the azure clients 
builder.Services.AddAzureClients(az=>
{
    // Establish the global defaults
    builder.ConfigureDefaults(Configuration.GetSection("AzureDefaults"));
    builder.UseCredential(new DefaultAzureCredential());

      // A named storage client with a different custom retry policy
    builder.AddBlobServiceClient(Configuration.GetSection("CustomStorage"))
      .WithName("CustomStorage")
      .ConfigureOptions(options => {
        options.Retry.Mode = Azure.Core.RetryMode.Exponential;
        options.Retry.MaxRetries = 5;
        options.Retry.MaxDelay = TimeSpan.FromSections(120);
      });
});

// and then later configure this:
//....

builder.Services.AddOrchardCms(orchard=>
{
     orchard.AddAzureShellsConfiguration(); // 
});

with the following configuration in appsettings.json (or any other configuration provider)

  "OrchardCore": {       
    "OrchardCore_Shells_Azure": {
      "ClientName" : "CustomStorage" // <<<< this is the important part
      "ContainerName": "some-container", // Set to the Azure Blob container name.
      "BasePath": "shells" // Optionally, set to a subdirectory inside your container.    
    }
  }

these examples are partially taken from:

https://devblogs.microsoft.com/azure-sdk/best-practices-for-using-azure-sdk-with-asp-net-core/

carlin-q-scott commented 1 year ago

@rikbosch That works too. It's a little more complicated to set up for us developers but I like the flexibility and the fact that you already implemented it 😄

rikbosch commented 1 year ago

I Updated the PR to fallback to the Default BlobServiceClient.

It allows for more complexity but could be as simple as:

// first configure the azure clients 
builder.Services.AddAzureClients(az=>
{
    builder.UseCredential(new DefaultAzureCredential());

      // a "Default" blobservice client
    builder.AddBlobServiceClient("storageUri");
});
  "OrchardCore": {       
    "OrchardCore_Shells_Azure": {
       // no ConnectionString or BlobServiceName set, so it will try to use the Default registered BlobServiceClient
      "ContainerName": "some-container", // Set to the Azure Blob container name.
      "BasePath": "shells" // Optionally, set to a subdirectory inside your container.    
    }
  }
rikbosch commented 1 year ago

Also, for performance reasons, a DefaultAzureCredential should be cached, as per the docs:

Acquired tokens are cached by the credential instance. Token lifetime and refreshing is handled automatically. Where possible, reuse credential instances to optimize cache effectiveness.

https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential.gettokenasync?view=azure-dotnet#definition

Piedone commented 7 months ago

Duplicated by https://github.com/OrchardCMS/OrchardCore/issues/15663. See https://github.com/OrchardCMS/OrchardCore/issues/15663#issuecomment-2041664331 for implementation suggestions.

MikeAlhayek commented 7 months ago

I did similar implementation using AzureAI module. Similar logic can be done here. We can also provide the configuration via configuration provider of via UI. The user can select the default settings or provide their own settings. The settings could be Default, API key or other supported methods.

Enable the AzureAI search module to evaluate the current setup in OC for this scenario