Skip to content

Commit 95e9f44

Browse files
authored
Allow calling AddSecurityHeadersPolicies multiple times (#250)
* Add test for expected behaviour * Add support for multiple configuration * Alternative approach * Revised approach * Remove incorrect comment
1 parent 0456c1e commit 95e9f44

File tree

5 files changed

+369
-5
lines changed

5 files changed

+369
-5
lines changed

src/NetEscapades.AspNetCore.SecurityHeaders/SecurityHeadersMiddlewareExtensions.cs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
24
using NetEscapades.AspNetCore.SecurityHeaders;
35
using NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;
46

@@ -28,7 +30,7 @@ public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder ap
2830
throw new ArgumentNullException(nameof(policies));
2931
}
3032

31-
var opts = app.ApplicationServices.GetService(typeof(CustomHeaderOptions)) as CustomHeaderOptions;
33+
var opts = GetOptions(app.ApplicationServices);
3234
return app.UseMiddleware(policies, opts);
3335
}
3436

@@ -65,7 +67,7 @@ public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder ap
6567
throw new ArgumentNullException(nameof(app));
6668
}
6769

68-
var options = app.ApplicationServices.GetService(typeof(CustomHeaderOptions)) as CustomHeaderOptions;
70+
var options = GetOptions(app.ApplicationServices);
6971
var policy = options?.DefaultPolicy ?? new HeaderPolicyCollection().AddDefaultSecurityHeaders();
7072

7173
return app.UseMiddleware(policy, options);
@@ -89,7 +91,7 @@ public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder ap
8991
throw new ArgumentNullException(nameof(policyName));
9092
}
9193

92-
var options = app.ApplicationServices.GetService(typeof(CustomHeaderOptions)) as CustomHeaderOptions;
94+
var options = GetOptions(app.ApplicationServices);
9395
var policy = options?.GetPolicy(policyName);
9496
if (policy is null)
9597
{
@@ -109,4 +111,44 @@ private static IApplicationBuilder UseMiddleware(
109111
{
110112
return app.UseMiddleware<SecurityHeadersMiddleware>(options ?? new(), policies);
111113
}
114+
115+
/// <summary>
116+
/// Get the <see cref="CustomHeaderOptions"/> to use. Internal for testing.
117+
/// </summary>
118+
/// <param name="services">The <see cref="IServiceProvider"/> to use</param>
119+
/// <returns>The options to use in the middleware</returns>
120+
internal static CustomHeaderOptions? GetOptions(IServiceProvider services)
121+
{
122+
// Only choose the _last_ directly registered CustomHeaderOptions
123+
var allOptions = services.GetServices<CustomHeaderOptions>();
124+
if (allOptions is null)
125+
{
126+
return null;
127+
}
128+
129+
CustomHeaderOptions? current = null;
130+
foreach (var next in allOptions)
131+
{
132+
if (next is null)
133+
{
134+
continue;
135+
}
136+
137+
if (current is null)
138+
{
139+
current = next;
140+
continue;
141+
}
142+
143+
// overwrite/merge everything
144+
current.DefaultPolicy = next.DefaultPolicy ?? current.DefaultPolicy;
145+
current.PolicySelector = next.PolicySelector ?? current.PolicySelector;
146+
foreach (var kvp in next.NamedPolicyCollections)
147+
{
148+
current.NamedPolicyCollections[kvp.Key] = kvp.Value;
149+
}
150+
}
151+
152+
return current;
153+
}
112154
}

src/NetEscapades.AspNetCore.SecurityHeaders/ServiceCollectionExtensions.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,36 @@ public static SecurityHeaderPolicyBuilder AddSecurityHeaderPolicies(this IServic
2626
return new SecurityHeaderPolicyBuilder(options);
2727
}
2828

29+
/// <summary>
30+
/// Creates a builder for configuring security header policies.
31+
/// </summary>
32+
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
33+
/// <param name="configure">A configuration method to configure your header policies</param>
34+
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
35+
public static IServiceCollection AddSecurityHeaderPolicies(
36+
this IServiceCollection services,
37+
Action<SecurityHeaderPolicyBuilder> configure)
38+
{
39+
if (services == null)
40+
{
41+
throw new ArgumentNullException(nameof(services));
42+
}
43+
44+
if (configure == null)
45+
{
46+
throw new ArgumentNullException(nameof(configure));
47+
}
48+
49+
services.AddSingleton(_ =>
50+
{
51+
var options = new CustomHeaderOptions();
52+
configure(new SecurityHeaderPolicyBuilder(options));
53+
return options;
54+
});
55+
56+
return services;
57+
}
58+
2959
/// <summary>
3060
/// Creates a builder for configuring security header policies.
3161
/// </summary>
@@ -47,9 +77,9 @@ public static IServiceCollection AddSecurityHeaderPolicies(
4777
throw new ArgumentNullException(nameof(configure));
4878
}
4979

50-
var options = new CustomHeaderOptions();
51-
services.AddSingleton<CustomHeaderOptions>(provider =>
80+
services.AddSingleton(provider =>
5281
{
82+
var options = new CustomHeaderOptions();
5383
configure(new SecurityHeaderPolicyBuilder(options), provider);
5484
return options;
5585
});

test/NetEscapades.AspNetCore.SecurityHeaders.Test/PublicApiTest.PublicApiHasNotChanged.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ namespace Microsoft.Extensions.DependencyInjection
289289
public static class ServiceCollectionExtensions
290290
{
291291
public static NetEscapades.AspNetCore.SecurityHeaders.Infrastructure.SecurityHeaderPolicyBuilder AddSecurityHeaderPolicies(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { }
292+
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddSecurityHeaderPolicies(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<NetEscapades.AspNetCore.SecurityHeaders.Infrastructure.SecurityHeaderPolicyBuilder> configure) { }
292293
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddSecurityHeaderPolicies(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<NetEscapades.AspNetCore.SecurityHeaders.Infrastructure.SecurityHeaderPolicyBuilder, System.IServiceProvider> configure) { }
293294
}
294295
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
using System;
2+
using FluentAssertions;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;
6+
7+
namespace NetEscapades.AspNetCore.SecurityHeaders.Test;
8+
9+
public class SecurityHeadersMiddlewareExtensionsTests
10+
{
11+
[Test]
12+
public void AddSecurityHeaderPolicies_NullPolicyByDefault()
13+
{
14+
var serviceCollection = new ServiceCollection();
15+
var provider = serviceCollection.BuildServiceProvider();
16+
var opts = SecurityHeadersMiddlewareExtensions.GetOptions(provider);
17+
18+
opts.Should().BeNull();
19+
}
20+
21+
[Test]
22+
[MatrixDataSource]
23+
public void AddSecurityHeaderPolicies_OverwritesPreviousDefault(
24+
[Matrix] RegistrationType first,
25+
[Matrix] RegistrationType second)
26+
{
27+
var serviceCollection = new ServiceCollection();
28+
HeaderPolicyCollection? policy1 = null;
29+
switch (first)
30+
{
31+
case RegistrationType.Direct:
32+
serviceCollection.AddSecurityHeaderPolicies().SetDefaultPolicy(p => { policy1 = p; });
33+
break;
34+
case RegistrationType.Func:
35+
serviceCollection.AddSecurityHeaderPolicies(o => o.SetDefaultPolicy(p => { policy1 = p; }));
36+
break;
37+
case RegistrationType.FuncWithProvider:
38+
serviceCollection.AddSecurityHeaderPolicies((o, _) => o.SetDefaultPolicy(p => { policy1 = p; }));
39+
break;
40+
}
41+
42+
HeaderPolicyCollection? policy2 = null;
43+
switch (first)
44+
{
45+
case RegistrationType.Direct:
46+
serviceCollection.AddSecurityHeaderPolicies().SetDefaultPolicy(p => { policy2 = p; });
47+
break;
48+
case RegistrationType.Func:
49+
serviceCollection.AddSecurityHeaderPolicies(o => o.SetDefaultPolicy(p => { policy2 = p; }));
50+
break;
51+
case RegistrationType.FuncWithProvider:
52+
serviceCollection.AddSecurityHeaderPolicies((o, _) => o.SetDefaultPolicy(p => { policy2 = p; }));
53+
break;
54+
}
55+
56+
var provider = serviceCollection.BuildServiceProvider();
57+
var opts = SecurityHeadersMiddlewareExtensions.GetOptions(provider);
58+
59+
policy1.Should().NotBeNull();
60+
policy2.Should().NotBeNull();
61+
opts.Should().NotBeNull();
62+
opts.DefaultPolicy.Should().BeSameAs(policy2);
63+
}
64+
65+
[Test]
66+
public void AddSecurityHeaderPolicies_OverwritesMultiplePreviousDefault()
67+
{
68+
var serviceCollection = new ServiceCollection();
69+
HeaderPolicyCollection? policy1 = null;
70+
serviceCollection.AddSecurityHeaderPolicies().SetDefaultPolicy(p => { policy1 = p; });
71+
HeaderPolicyCollection? policy2 = null;
72+
serviceCollection.AddSecurityHeaderPolicies(o => o.SetDefaultPolicy(p => { policy2 = p; }));
73+
HeaderPolicyCollection? policy3 = null;
74+
serviceCollection.AddSecurityHeaderPolicies((o, _) => o.SetDefaultPolicy(p => { policy3 = p; }));
75+
76+
var provider = serviceCollection.BuildServiceProvider();
77+
var opts = SecurityHeadersMiddlewareExtensions.GetOptions(provider);
78+
79+
policy1.Should().NotBeNull();
80+
policy2.Should().NotBeNull();
81+
policy3.Should().NotBeNull();
82+
opts.Should().NotBeNull();
83+
opts.DefaultPolicy.Should().BeSameAs(policy3);
84+
}
85+
86+
[Test]
87+
[MatrixDataSource]
88+
public void AddSecurityHeaderPolicies_OverwritesPreviousPolicySelector(
89+
[Matrix] RegistrationType first,
90+
[Matrix] RegistrationType second)
91+
{
92+
var serviceCollection = new ServiceCollection();
93+
Func<PolicySelectorContext, IReadOnlyHeaderPolicyCollection> selector1 = x => x.DefaultPolicy;
94+
Func<PolicySelectorContext, IReadOnlyHeaderPolicyCollection> selector2 = x => x.DefaultPolicy;
95+
SetSelector(serviceCollection, first, selector1);
96+
SetSelector(serviceCollection, second, selector2);
97+
var provider = serviceCollection.BuildServiceProvider();
98+
var opts = SecurityHeadersMiddlewareExtensions.GetOptions(provider);
99+
100+
opts.Should().NotBeNull();
101+
opts.PolicySelector.Should().BeSameAs(selector2);
102+
103+
static void SetSelector(ServiceCollection services, RegistrationType type,
104+
Func<PolicySelectorContext, IReadOnlyHeaderPolicyCollection> selector)
105+
{
106+
switch (type)
107+
{
108+
case RegistrationType.Direct:
109+
services.AddSecurityHeaderPolicies().SetPolicySelector(selector);
110+
break;
111+
case RegistrationType.Func:
112+
services.AddSecurityHeaderPolicies(o => o.SetPolicySelector(selector));
113+
break;
114+
case RegistrationType.FuncWithProvider:
115+
services.AddSecurityHeaderPolicies((o, _) => o.SetPolicySelector(selector));
116+
break;
117+
default:
118+
throw new ArgumentOutOfRangeException(nameof(type), type, null);
119+
}
120+
}
121+
}
122+
123+
[Test]
124+
public void AddSecurityHeaderPolicies_OverwritesAndIncludesPreviousNamedPolicies()
125+
{
126+
var serviceCollection = new ServiceCollection();
127+
128+
var name1 = "name1";
129+
var name2 = "name2";
130+
var name3 = "name3";
131+
var replacement = new HeaderPolicyCollection();
132+
serviceCollection.AddSecurityHeaderPolicies().AddPolicy(name1, new HeaderPolicyCollection());
133+
serviceCollection.AddSecurityHeaderPolicies(o => o.AddPolicy(name2, new HeaderPolicyCollection()));
134+
serviceCollection.AddSecurityHeaderPolicies((o, _) => o.AddPolicy(name3, new HeaderPolicyCollection()));
135+
serviceCollection.AddSecurityHeaderPolicies().AddPolicy(name3, replacement);
136+
137+
var provider = serviceCollection.BuildServiceProvider();
138+
var opts = SecurityHeadersMiddlewareExtensions.GetOptions(provider);
139+
140+
opts.Should().NotBeNull();
141+
opts.NamedPolicyCollections.Should().ContainKeys(name1, name2, name3).And.HaveCount(3);
142+
opts.NamedPolicyCollections[name3].Should().BeSameAs(replacement);
143+
}
144+
145+
public enum RegistrationType
146+
{
147+
Direct,
148+
Func,
149+
FuncWithProvider
150+
}
151+
}

0 commit comments

Comments
 (0)