Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageVersion Include="MailKit" Version="4.14.0" />
<PackageVersion Include="Markdig" Version="0.42.0" />
<PackageVersion Include="Microsoft.Extensions.Azure" Version="1.13.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
<PackageVersion Include="Microsoft.Identity.Web" Version="3.14.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.14.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
Expand Down
21 changes: 21 additions & 0 deletions OrchardCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Taxonomies.Core
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Flows.Core", "src\OrchardCore\OrchardCore.Flows.Core\OrchardCore.Flows.Core.csproj", "{F7F3AFBD-8045-49D3-BA06-504954D6910B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Redis.Azure", "src\OrchardCore.Modules\OrchardCore.Redis.Azure\OrchardCore.Redis.Azure.csproj", "{61848F3E-972A-4AD3-965E-DC42E690BF05}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Azure.Core", "src\OrchardCore\OrchardCore.Azure.Core\OrchardCore.Azure.Core.csproj", "{903FFEBA-B69D-4464-8391-9240C9EB8384}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Azure", "src\OrchardCore.Modules\OrchardCore.Azure\OrchardCore.Azure.csproj", "{5BA3337B-2266-40A8-87A2-C4497D260FE0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1481,6 +1487,18 @@ Global
{F7F3AFBD-8045-49D3-BA06-504954D6910B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7F3AFBD-8045-49D3-BA06-504954D6910B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7F3AFBD-8045-49D3-BA06-504954D6910B}.Release|Any CPU.Build.0 = Release|Any CPU
{61848F3E-972A-4AD3-965E-DC42E690BF05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61848F3E-972A-4AD3-965E-DC42E690BF05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61848F3E-972A-4AD3-965E-DC42E690BF05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61848F3E-972A-4AD3-965E-DC42E690BF05}.Release|Any CPU.Build.0 = Release|Any CPU
{903FFEBA-B69D-4464-8391-9240C9EB8384}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{903FFEBA-B69D-4464-8391-9240C9EB8384}.Debug|Any CPU.Build.0 = Debug|Any CPU
{903FFEBA-B69D-4464-8391-9240C9EB8384}.Release|Any CPU.ActiveCfg = Release|Any CPU
{903FFEBA-B69D-4464-8391-9240C9EB8384}.Release|Any CPU.Build.0 = Release|Any CPU
{5BA3337B-2266-40A8-87A2-C4497D260FE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BA3337B-2266-40A8-87A2-C4497D260FE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BA3337B-2266-40A8-87A2-C4497D260FE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BA3337B-2266-40A8-87A2-C4497D260FE0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1734,6 +1752,9 @@ Global
{51C07EE9-9420-4BFE-911B-9F4326A0F83B} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{6A379AE9-B468-4D89-82B2-0C350AB712F9} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{F7F3AFBD-8045-49D3-BA06-504954D6910B} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{61848F3E-972A-4AD3-965E-DC42E690BF05} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{903FFEBA-B69D-4464-8391-9240C9EB8384} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{5BA3337B-2266-40A8-87A2-C4497D260FE0} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341}
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ nav:
- Display Management: reference/modules/DisplayManagement/README.md
- Audit Trail: reference/modules/AuditTrail/README.md
- Auto Setup: reference/modules/AutoSetup/README.md
- Azure: reference/modules/Azure/README.md
- Features: reference/modules/Features/README.md
- Contents: reference/modules/Contents/README.md
- Configuration: reference/modules/Configuration/README.md
Expand Down Expand Up @@ -271,6 +272,7 @@ nav:
- Razor Helpers: reference/modules/Razor/README.md
- Recipes: reference/modules/Recipes/README.md
- Redis: reference/modules/Redis/README.md
- Redis Azure: reference/modules/Redis.Azure/README.md
- Remote Deployment: reference/modules/Deployment.Remote/README.md
- Response Compression: reference/modules/ResponseCompression/README.md
- Roles: reference/modules/Roles/README.md
Expand Down
10 changes: 10 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Azure/Manifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using OrchardCore.Modules.Manifest;

[assembly: Module(
Name = "OrchardCore.Azure",
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion,
Description = "Provides a way to manage Azure credentials",
Category = "Azure"
)]
12 changes: 12 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Azure/OrchardCore.Azure.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Abstractions\OrchardCore.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Azure.Core\OrchardCore.Azure.Core.csproj" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Azure/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Azure.Core;
using OrchardCore.Modules;

namespace OrchardCore.Azure;

public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddAzureOptions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Azure.Core;

namespace OrchardCore.Redis.Azure;

public sealed class AzureRedisTokenProvider : IRedisTokenProvider
{
private readonly IOptionsMonitor<AzureOptions> _options;
private readonly ILogger _logger;

public AzureRedisTokenProvider(
IOptionsMonitor<AzureOptions> options,
ILogger<AzureRedisTokenProvider> logger)
{
_options = options;
_logger = logger;
}

public async Task<string> GetTokenAsync()
{
var redisOptions = _options.Get("Redis");

var scopes = redisOptions.GetProperty<string[]>("Scopes");

if (scopes is null || scopes.Length == 0)
{
_logger.LogWarning("No scope configured for Azure Redis authentication, returning empty token.");

return null;
}

TokenCredential credential = redisOptions.AuthenticationType switch
{
AzureAuthenticationType.Default => new DefaultAzureCredential(),
AzureAuthenticationType.ManagedIdentity => new ManagedIdentityCredential(),
AzureAuthenticationType.AzureCli => new AzureCliCredential(),
_ => throw new NotSupportedException($"Authentication type {redisOptions.AuthenticationType} is not supported")
};

var requestContext = new TokenRequestContext(scopes);

var result = await credential.GetTokenAsync(requestContext, CancellationToken.None);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cf previous comment, this should be in the OC.Azure module.


return result.Token;
}
}
15 changes: 15 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Redis.Azure/Manifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using OrchardCore.Modules.Manifest;

[assembly: Module(
Name = "Azure Redis Cache",
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion,
Description = "Distributed cache using Azure Redis.",
Dependencies =
[
"OrchardCore.Redis",
"OrchardCore.Azure",
],
Category = "Distributed"
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- NuGet properties-->
<Title>OrchardCore Redis Azure</Title>
<Description>
$(OCFrameworkDescription)

Provides Azure Redis features for configuration, cache, bus and lock.
</Description>
<PackageTags>$(PackageTags) OrchardCoreFramework Infrastructure</PackageTags>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Abstractions\OrchardCore.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Azure.Core\OrchardCore.Azure.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Infrastructure.Abstractions\OrchardCore.Infrastructure.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Redis.Abstractions\OrchardCore.Redis.Abstractions.csproj" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Redis.Azure/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Modules;

namespace OrchardCore.Redis.Azure;

public sealed class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IRedisTokenProvider, AzureRedisTokenProvider>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
<PropertyGroup>
<!-- NuGet properties-->
<Title>OrchardCore Redis</Title>
<Description>$(OCFrameworkDescription)
<Description>
$(OCFrameworkDescription)

Provides Redis features for configuration, cache, bus and lock.</Description>
Provides Redis features for configuration, cache, bus and lock.
</Description>
<PackageTags>$(PackageTags) OrchardCoreFramework Infrastructure</PackageTags>
</PropertyGroup>

Expand All @@ -14,6 +16,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Azure.Core\OrchardCore.Azure.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Infrastructure.Abstractions\OrchardCore.Infrastructure.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Redis.Abstractions\OrchardCore.Redis.Abstractions.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
Expand All @@ -14,14 +15,19 @@ public sealed class RedisDatabaseFactory : IRedisDatabaseFactory, IDisposable
private static volatile int _registered;
private static volatile int _refCount;

private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _lifetime;
private readonly ILogger _logger;

public RedisDatabaseFactory(IHostApplicationLifetime lifetime, ILogger<RedisDatabaseFactory> logger)
public RedisDatabaseFactory(
IServiceProvider serviceProvider,
IHostApplicationLifetime lifetime,
ILogger<RedisDatabaseFactory> logger)
{
Interlocked.Increment(ref _refCount);

_serviceProvider = serviceProvider;
_lifetime = lifetime;

if (Interlocked.CompareExchange(ref _registered, 1, 0) == 0)
{
_lifetime.ApplicationStopped.Register(Release);
Expand All @@ -30,25 +36,59 @@ public RedisDatabaseFactory(IHostApplicationLifetime lifetime, ILogger<RedisData
_logger = logger;
}

public Task<IDatabase> CreateAsync(RedisOptions options) =>
_factories.GetOrAdd(options.Configuration, new Lazy<Task<IDatabase>>(async () =>
public Task<IDatabase> CreateAsync(RedisOptions options)
{
return _factories.GetOrAdd(options.ConnectionIdentifier, new Lazy<Task<IDatabase>>(async () =>
{
try
var provider = _serviceProvider.GetService<IRedisTokenProvider>();

var config = options.ConfigurationOptions;

if (provider is null)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Creating a new instance of '{Name}'. A single instance per configuration should be created across tenants. Total instances prior creating is '{Count}'.", nameof(ConnectionMultiplexer), _factories.Count);
}
var connection = await ConnectionMultiplexer.ConnectAsync(config);

return connection.GetDatabase();
}

return (await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions)).GetDatabase();
var token = await provider.GetTokenAsync();

if (!string.IsNullOrEmpty(token))
{
config.Password = token;
}
catch (Exception e)

var attempt = 0;

while (attempt < 3)
{
_logger.LogError(e, "Unable to connect to Redis.");
try
{
var connection = await ConnectionMultiplexer.ConnectAsync(config);
return connection.GetDatabase();
}
catch (RedisConnectionException ex) when (ex.Message.Contains("WRONGPASS") || ex.Message.Contains("NOAUTH"))
{
attempt++;
_logger.LogWarning(ex, "Redis authentication failed, retry attempt {Attempt}", attempt);

if (provider == null)
{
break;
}

return null;
token = await provider.GetTokenAsync();

if (!string.IsNullOrEmpty(token))
{
config.Password = token;
}
}
}

throw new InvalidOperationException("Unable to authenticate to Redis after multiple attempts.");
})).Value;
}

public void Dispose()
{
Expand All @@ -63,7 +103,6 @@ internal static void Release()
if (Interlocked.CompareExchange(ref _refCount, 0, 0) == 0)
{
var factories = _factories.Values.ToArray();

_factories.Clear();

foreach (var factory in factories)
Expand Down
6 changes: 6 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Redis/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
Expand All @@ -11,6 +12,7 @@
using OrchardCore.Environment.Cache;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Environment.Shell.Scope;
using OrchardCore.Locking.Distributed;
using OrchardCore.Modules;
using OrchardCore.Redis.Options;
Expand Down Expand Up @@ -55,6 +57,10 @@ public override void ConfigureServices(IServiceCollection services)

services.Configure<RedisOptions>(options =>
{
var protectorProvider = ShellScope.Services.GetRequiredService<IDataProtectionProvider>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like a broken pattern to get and ambient shellscope. IF we can't access the services from this lambda then it's either wrong or it can't be a lambda.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Don't like it either. We are doing the same thing in the Users module

var protector = protectorProvider.CreateProtector("RedisOptions");

options.ConnectionIdentifier = protector.Protect(configuration);
options.Configuration = configuration;
options.ConfigurationOptions = configurationOptions;
options.InstancePrefix = instancePrefix;
Expand Down
Loading