From 4e8eb5c073beb18526034c84a75bece5d8544992 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 12 Jun 2024 15:29:43 +0200 Subject: [PATCH] Use options pattern for magic links and application overrides (#613) --- src/Api/Endpoints/Magic.cs | 12 +++++------- src/Api/Extensions/MagicLinkBootstrap.cs | 8 +++++--- src/Api/Program.cs | 4 +++- .../Overrides/ApplicationOverridesExtensions.cs | 17 ----------------- .../Overrides/ApplicationOverridesOptions.cs | 15 +++++++++++++++ src/Service/MagicLinks/MagicLinkService.cs | 16 +++++++--------- src/Service/MagicLinks/MagicLinksOptions.cs | 6 ++++++ 7 files changed, 41 insertions(+), 37 deletions(-) delete mode 100644 src/Common/Overrides/ApplicationOverridesExtensions.cs create mode 100644 src/Common/Overrides/ApplicationOverridesOptions.cs create mode 100644 src/Service/MagicLinks/MagicLinksOptions.cs diff --git a/src/Api/Endpoints/Magic.cs b/src/Api/Endpoints/Magic.cs index cb57d0923..1fa71e6c1 100644 --- a/src/Api/Endpoints/Magic.cs +++ b/src/Api/Endpoints/Magic.cs @@ -4,6 +4,7 @@ using System.Threading.RateLimiting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.Options; using Passwordless.Api.Authorization; using Passwordless.Api.OpenApi; using Passwordless.Common.MagicLinks.Models; @@ -12,7 +13,6 @@ using Passwordless.Service.Helpers; using Passwordless.Service.MagicLinks; using Passwordless.Service.MagicLinks.Extensions; -using Passwordless.Service.Models; using static Microsoft.AspNetCore.Http.Results; namespace Passwordless.Api.Endpoints; @@ -32,13 +32,11 @@ public static void AddMagicRateLimiterPolicy(this RateLimiterOptions builder) => { var tenant = context.User.FindFirstValue(CustomClaimTypes.AccountName) ?? ""; - var isRateLimitBypassed = context.RequestServices - .GetRequiredService() - .GetSection("ApplicationOverrides") - .GetApplicationOverrides(tenant) - .IsRateLimitBypassEnabled; + var applicationOverridesOptions = context.RequestServices + .GetRequiredService>(); - if (isRateLimitBypassed) + var applicationOverrides = applicationOverridesOptions.Value.GetApplication(tenant); + if (applicationOverrides.IsRateLimitBypassEnabled) return RateLimitPartition.GetNoLimiter(tenant); return RateLimitPartition.GetFixedWindowLimiter(tenant, _ => diff --git a/src/Api/Extensions/MagicLinkBootstrap.cs b/src/Api/Extensions/MagicLinkBootstrap.cs index 828fbedf7..2e25f481d 100644 --- a/src/Api/Extensions/MagicLinkBootstrap.cs +++ b/src/Api/Extensions/MagicLinkBootstrap.cs @@ -4,7 +4,9 @@ namespace Passwordless.Api.Extensions; public static class MagicLinkBootstrap { - public static IServiceCollection AddMagicLinks(this IServiceCollection serviceCollection) => - serviceCollection - .AddScoped(); + public static void AddMagicLinks(this WebApplicationBuilder builder) + { + builder.Services.AddOptions().BindConfiguration("MagicLinks"); + builder.Services.AddScoped(); + } } \ No newline at end of file diff --git a/src/Api/Program.cs b/src/Api/Program.cs index d4aa1567a..44c3b778b 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -19,6 +19,7 @@ using Passwordless.Common.HealthChecks; using Passwordless.Common.Logging; using Passwordless.Common.Middleware.SelfHosting; +using Passwordless.Common.Overrides; using Passwordless.Common.Services.Mail; using Passwordless.Service; using Passwordless.Service.EventLog; @@ -102,7 +103,8 @@ builder.AddPasswordlessHealthChecks(); -builder.Services.AddMagicLinks(); +builder.Services.AddOptions().BindConfiguration("ApplicationOverrides"); +builder.AddMagicLinks(); WebApplication app = builder.Build(); diff --git a/src/Common/Overrides/ApplicationOverridesExtensions.cs b/src/Common/Overrides/ApplicationOverridesExtensions.cs deleted file mode 100644 index 62deace5b..000000000 --- a/src/Common/Overrides/ApplicationOverridesExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Passwordless.Common.Overrides; - -/// -/// Extensions for . -/// -public static class ApplicationOverridesExtensions -{ - /// - /// Gets overrides for the specified application from the configuration. - /// - public static ApplicationOverrides GetApplicationOverrides( - this IConfiguration configuration, - string applicationId) => - configuration - .GetSection(applicationId) - .Get() ?? new ApplicationOverrides(); -} \ No newline at end of file diff --git a/src/Common/Overrides/ApplicationOverridesOptions.cs b/src/Common/Overrides/ApplicationOverridesOptions.cs new file mode 100644 index 000000000..0d8a79f71 --- /dev/null +++ b/src/Common/Overrides/ApplicationOverridesOptions.cs @@ -0,0 +1,15 @@ +namespace Passwordless.Common.Overrides; + +public class ApplicationOverridesOptions : Dictionary +{ + /// + /// Gets the application overrides for the specified tenant. If the application is not found, an instance with + /// default values is returned. + /// + /// + /// + public ApplicationOverrides GetApplication(string tenant) + { + return this.TryGetValue(tenant, out var overrides) ? overrides : new ApplicationOverrides(); + } +} \ No newline at end of file diff --git a/src/Service/MagicLinks/MagicLinkService.cs b/src/Service/MagicLinks/MagicLinkService.cs index 7c8edbbdb..83182743a 100644 --- a/src/Service/MagicLinks/MagicLinkService.cs +++ b/src/Service/MagicLinks/MagicLinkService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using Passwordless.Common.MagicLinks.Models; using Passwordless.Common.Overrides; using Passwordless.Common.Services.Mail; @@ -11,16 +11,13 @@ namespace Passwordless.Service.MagicLinks; public class MagicLinkService( TimeProvider timeProvider, - IConfiguration configuration, + IOptionsSnapshot magicLinksOptions, + IOptionsSnapshot applicationOverridesOptions, ITenantStorage tenantStorage, IFido2Service fido2Service, IMailProvider mailProvider, IEventLogger eventLogger) { - private TimeSpan NewAccountTimeout { get; } = - configuration.GetSection("MagicLinks").GetValue(nameof(NewAccountTimeout)) ?? - TimeSpan.FromHours(24); - private async Task EnforceQuotaAsync(MagicLinkTokenRequest request) { var now = timeProvider.GetUtcNow().UtcDateTime; @@ -28,16 +25,17 @@ private async Task EnforceQuotaAsync(MagicLinkTokenRequest request) var accountAge = now - account.CreatedAt; // Check bypass - if (configuration.GetApplicationOverrides(account.AcountName).IsMagicLinkQuotaBypassEnabled) + var applicationOverrides = applicationOverridesOptions.Value.GetApplication(account.AcountName); + if (applicationOverrides.IsMagicLinkQuotaBypassEnabled) return; // Newly created accounts can only send magic links to the admin email address - if (accountAge < NewAccountTimeout && + if (accountAge < magicLinksOptions.Value.NewAccountTimeout && !account.AdminEmails.Contains(request.EmailAddress.Address, StringComparer.OrdinalIgnoreCase)) { throw new ApiException( "magic_link_email_admin_address_only", - $"Because your application has been created less than {(int)NewAccountTimeout.TotalHours} hours ago, " + + $"Because your application has been created less than {(int)magicLinksOptions.Value.NewAccountTimeout.TotalHours} hours ago, " + "you can only request magic links to the admin email address.", 403 ); diff --git a/src/Service/MagicLinks/MagicLinksOptions.cs b/src/Service/MagicLinks/MagicLinksOptions.cs new file mode 100644 index 000000000..bf28b7e18 --- /dev/null +++ b/src/Service/MagicLinks/MagicLinksOptions.cs @@ -0,0 +1,6 @@ +namespace Passwordless.Service.MagicLinks; + +public class MagicLinksOptions +{ + public TimeSpan NewAccountTimeout { get; set; } = TimeSpan.FromHours(24); +} \ No newline at end of file