Skip to content

Commit e669110

Browse files
authored
Authz hardening: secure registry load, tighter fallback policy, and policy change events (#30)
Changed: Fix auth registry being loaded in insecure mode Changed: authPolicyRegistryFactory plugin Changed: Emit PolicyChanged when policies update Changed: Restrict fallback policy to stream access Changed: Rename PolicyType to DefaultPolicyType Changed: Explicitly deny stream operations in fallback policy, skip PolicyRegistry setup when a custom authz plugin is configured Changed: StreamBasedAuthPolicyRegistry tests Changed: Use custom Publisher and longer timeouts Signed-off-by: JOSE FUXA <[email protected]>
1 parent 8f42ea0 commit e669110

24 files changed

+1368
-204
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using EventStore.Common.Exceptions;
7+
using EventStore.Core;
8+
using EventStore.Core.Authorization.AuthorizationPolicies;
9+
using EventStore.Core.Bus;
10+
using EventStore.PluginHosting;
11+
using EventStore.Plugins;
12+
using EventStore.Plugins.Subsystems;
13+
using Microsoft.AspNetCore.Builder;
14+
using Microsoft.Extensions.Configuration;
15+
using Microsoft.Extensions.DependencyInjection;
16+
using Serilog;
17+
18+
namespace EventStore.ClusterNode;
19+
20+
public class AuthorizationPolicyRegistryFactory : SubsystemsPlugin
21+
{
22+
private readonly ILogger _logger = Log.ForContext<AuthorizationPolicyRegistryFactory>();
23+
private readonly IPolicySelectorFactory[] _pluginSelectorFactories = [];
24+
private readonly Func<IPublisher, IAuthorizationPolicyRegistry> _createRegistry;
25+
private IAuthorizationPolicyRegistry? _authorizationPolicyRegistry;
26+
27+
public AuthorizationPolicyRegistryFactory(ClusterVNodeOptions options, IConfiguration configuration,
28+
PluginLoader pluginLoader)
29+
{
30+
if (options.Application.Insecure)
31+
{
32+
_createRegistry = _ => new StaticAuthorizationPolicyRegistry([]);
33+
return;
34+
}
35+
36+
// Load up all policy selectors in the plugins directory
37+
var factories = pluginLoader.Load<IPolicySelectorFactory>();
38+
_pluginSelectorFactories = factories?
39+
.Select(x =>
40+
{
41+
_logger.Information("Loaded Authorization Policy plugin: {plugin}.", x.CommandLineName);
42+
return x;
43+
}).ToArray() ?? [];
44+
45+
// Set up the legacy policy selector factory
46+
var allowAnonymousEndpointAccess = options.Application.AllowAnonymousEndpointAccess;
47+
var allowAnonymousStreamAccess = options.Application.AllowAnonymousStreamAccess;
48+
var overrideAnonymousGossipEndpointAccess = options.Application.OverrideAnonymousEndpointAccessForGossip;
49+
var legacyPolicyFactory = new LegacyPolicySelectorFactory(
50+
allowAnonymousEndpointAccess,
51+
allowAnonymousStreamAccess,
52+
overrideAnonymousGossipEndpointAccess);
53+
54+
// Check if there is a default policy type. Use this if the settings stream is empty
55+
var defaultPolicyType =
56+
configuration.GetValue<string>("EventStore:Authorization:DefaultPolicyType") ?? string.Empty;
57+
AuthorizationPolicySettings defaultSettings;
58+
if (!string.IsNullOrEmpty(defaultPolicyType))
59+
{
60+
if (_pluginSelectorFactories.Any(x => x.CommandLineName == defaultPolicyType))
61+
{
62+
defaultSettings = new AuthorizationPolicySettings(defaultPolicyType);
63+
}
64+
else
65+
{
66+
throw new InvalidConfigurationException(
67+
$"No authorization policy with the name '{defaultPolicyType}' has been registered. " +
68+
$"Available authorization policies are: {string.Join(", ", _pluginSelectorFactories.Select(x => x.CommandLineName))}");
69+
}
70+
}
71+
else
72+
{
73+
// No default policy type configured. Use ACLs.
74+
defaultSettings = new AuthorizationPolicySettings(LegacyPolicySelectorFactory.LegacyPolicySelectorName);
75+
}
76+
77+
// There are no plugins and a default policy type wasn't configured.
78+
// Don't use the stream based registry
79+
if (_pluginSelectorFactories.Length == 0)
80+
{
81+
_logger.Information("No authorization policy plugins found. Only ACLs will be used.");
82+
_createRegistry = publisher =>
83+
new StaticAuthorizationPolicyRegistry([legacyPolicyFactory.Create(publisher)]);
84+
return;
85+
}
86+
87+
_logger.Information("The default authorization policy settings are: {settings}", defaultSettings);
88+
_createRegistry = publisher => new StreamBasedAuthorizationPolicyRegistry(publisher,
89+
legacyPolicyFactory.Create(publisher), _pluginSelectorFactories, defaultSettings);
90+
}
91+
92+
// Use a factory rather than ConfigureApplication because the authorization providers
93+
// are built (and requires this registry) before ConfigureApplication is called
94+
public IAuthorizationPolicyRegistry Create(IPublisher publisher)
95+
{
96+
if (_authorizationPolicyRegistry is not null)
97+
{
98+
return _authorizationPolicyRegistry;
99+
}
100+
101+
_authorizationPolicyRegistry = _createRegistry(publisher);
102+
return _authorizationPolicyRegistry;
103+
}
104+
105+
public override IReadOnlyList<ISubsystem> GetSubsystems()
106+
{
107+
var subsystems = new List<ISubsystem> { this };
108+
// ReSharper disable once SuspiciousTypeConversion.Global
109+
subsystems.AddRange(_pluginSelectorFactories.OfType<ISubsystem>());
110+
return subsystems.ToArray();
111+
}
112+
113+
public override Task Start()
114+
{
115+
return _authorizationPolicyRegistry!.Start();
116+
}
117+
118+
public override Task Stop()
119+
{
120+
return _authorizationPolicyRegistry!.Stop();
121+
}
122+
}

src/EventStore.ClusterNode/ClusterVNodeHostedService.cs

Lines changed: 18 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
using EventStore.Core.Authentication.InternalAuthentication;
1616
using EventStore.Core.Authentication.PassthroughAuthentication;
1717
using EventStore.Core.Authorization;
18-
using EventStore.Core.Bus;
1918
using EventStore.Core.Certificates;
2019
using EventStore.Core.Hashing;
2120
using EventStore.Core.LogAbstraction;
@@ -110,20 +109,20 @@ public ClusterVNodeHostedService(
110109
? _options.Application.Config
111110
: _options.Auth.AuthenticationConfig;
112111

113-
(_options, var policySelectorsFactory) = ConfigurePolicySelectorsFactory();
112+
(_options, var authProviderFactory) = GetAuthorizationProviderFactory();
114113
if (_options.Database.DbLogFormat == DbLogFormat.V2)
115114
{
116115
var logFormatFactory = new LogV2FormatAbstractorFactory();
117116
Node = ClusterVNode.Create(_options, logFormatFactory, GetAuthenticationProviderFactory(),
118-
GetAuthorizationProviderFactory(policySelectorsFactory),
117+
authProviderFactory,
119118
GetPersistentSubscriptionConsumerStrategyFactories(), certificateProvider,
120119
configuration);
121120
}
122121
else if (_options.Database.DbLogFormat == DbLogFormat.ExperimentalV3)
123122
{
124123
var logFormatFactory = new LogV3FormatAbstractorFactory();
125124
Node = ClusterVNode.Create(_options, logFormatFactory, GetAuthenticationProviderFactory(),
126-
GetAuthorizationProviderFactory(policySelectorsFactory),
125+
authProviderFactory,
127126
GetPersistentSubscriptionConsumerStrategyFactories(), certificateProvider,
128127
configuration);
129128
}
@@ -139,81 +138,26 @@ public ClusterVNodeHostedService(
139138
RegisterWebControllers(enabledNodeSubsystems);
140139
return;
141140

142-
(ClusterVNodeOptions, PolicySelectorsFactory) ConfigurePolicySelectorsFactory()
141+
(ClusterVNodeOptions, AuthorizationProviderFactory) GetAuthorizationProviderFactory()
143142
{
144143
if (_options.Application.Insecure)
145144
{
146-
return (_options, new PolicySelectorsFactory());
145+
return (_options, new AuthorizationProviderFactory(_ => new PassthroughAuthorizationProviderFactory()));
147146
}
148147

149-
var defaultPolicySelector = new LegacyPolicySelectorFactory(
150-
_options.Application.AllowAnonymousEndpointAccess,
151-
_options.Application.AllowAnonymousStreamAccess,
152-
_options.Application.OverrideAnonymousEndpointAccessForGossip);
153-
154-
// Temporary: get the policy plugin configuration
155-
// TODO: Allow specifying multiple policy selectors
156-
var policyPluginType =
157-
_options.ConfigurationRoot!.GetValue<string>("EventStore:Plugins:Authorization:PolicyType") ??
158-
string.Empty;
159-
160-
var policyPlugins = pluginLoader.Load<IPolicySelectorFactory>().ToArray();
161-
var policySelectors = new Dictionary<string, IPolicySelectorFactory>();
162-
foreach (var policyPlugin in policyPlugins)
163-
{
164-
try
165-
{
166-
var commandLine = policyPlugin.Name.Replace("Plugin", "").ToLowerInvariant();
167-
Log.Information(
168-
"Loaded authorization policy plugin: {plugin} version {version} (Command Line: {commandLine})",
169-
policyPlugin.Name, policyPlugin.Version, commandLine);
170-
policySelectors.Add(commandLine, policyPlugin);
171-
}
172-
catch (CompositionException ex)
173-
{
174-
Log.Error(ex, "Error loading authorization policy plugin.");
148+
var modifiedOptions = _options;
149+
if (_options.Auth.AuthorizationType.Equals("internal", StringComparison.InvariantCultureIgnoreCase)) {
150+
var registryFactory = new AuthorizationPolicyRegistryFactory(_options, configuration, pluginLoader);
151+
foreach (var authSubsystem in registryFactory.GetSubsystems()) {
152+
modifiedOptions = modifiedOptions.WithPlugableComponent(authSubsystem);
175153
}
176-
}
177154

178-
if (policyPluginType == string.Empty)
179-
{
180-
Log.Information("Using default authorization policy");
181-
return (_options, new PolicySelectorsFactory(defaultPolicySelector));
155+
var internalFactory = new AuthorizationProviderFactory(components =>
156+
new InternalAuthorizationProviderFactory(registryFactory.Create(components.MainQueue)));
157+
return (modifiedOptions, internalFactory);
182158
}
183-
if (!policySelectors.TryGetValue(policyPluginType, out var selectedPolicy))
184-
{
185-
throw new ApplicationInitializationException(
186-
$"The authorization policy plugin type {policyPluginType} is not recognised. If this is supposed " +
187-
$"to be provided by an authorization policy plugin, confirm the plugin DLL is located in {Locations.PluginsDirectory}." +
188-
Environment.NewLine +
189-
$"Valid options for authorization policies are: {string.Join(", ", policySelectors.Keys)}.");
190-
}
191-
192-
Log.Information("Using authorization policy plugin: {plugin} version {version}", selectedPolicy.Name,
193-
selectedPolicy.Version);
194-
// Policies will be applied in order, so the default should always be last
195-
var factory = new PolicySelectorsFactory([selectedPolicy, defaultPolicySelector]);
196159

197-
if (selectedPolicy is IPlugableComponent plugablePolicy)
198-
{
199-
return (_options.WithPlugableComponent(plugablePolicy), factory);
200-
}
201-
return (_options, factory);
202-
}
203-
204-
AuthorizationProviderFactory GetAuthorizationProviderFactory(PolicySelectorsFactory policySelectorsFactory)
205-
{
206-
if (_options.Application.Insecure)
207-
{
208-
return new AuthorizationProviderFactory(_ => new PassthroughAuthorizationProviderFactory());
209-
}
210-
var authorizationTypeToPlugin = new Dictionary<string, AuthorizationProviderFactory> {
211-
{
212-
"internal", new AuthorizationProviderFactory(components =>
213-
new InternalAuthorizationProviderFactory(policySelectorsFactory.Create(components))
214-
)
215-
}
216-
};
160+
var authorizationTypeToPlugin = new Dictionary<string, AuthorizationProviderFactory> { };
217161

218162
foreach (var potentialPlugin in pluginLoader.Load<IAuthorizationPlugin>())
219163
{
@@ -224,8 +168,9 @@ AuthorizationProviderFactory GetAuthorizationProviderFactory(PolicySelectorsFact
224168
"Loaded authorization plugin: {plugin} version {version} (Command Line: {commandLine})",
225169
potentialPlugin.Name, potentialPlugin.Version, commandLine);
226170
authorizationTypeToPlugin.Add(commandLine,
227-
new AuthorizationProviderFactory(_ =>
228-
potentialPlugin.GetAuthorizationProviderFactory(authorizationConfig)));
171+
new AuthorizationProviderFactory(
172+
_ => potentialPlugin.GetAuthorizationProviderFactory(authorizationConfig)
173+
));
229174
}
230175
catch (CompositionException ex)
231176
{
@@ -243,7 +188,7 @@ AuthorizationProviderFactory GetAuthorizationProviderFactory(PolicySelectorsFact
243188
$"Valid options for authorization are: {string.Join(", ", authorizationTypeToPlugin.Keys)}.");
244189
}
245190

246-
return factory;
191+
return (modifiedOptions, factory);
247192
}
248193

249194
static CompositionContainer FindPlugins()

src/EventStore.Core.Tests/Authorization/LegacyPolicyVerification.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading;
66
using System.Threading.Tasks;
77
using EventStore.Core.Authorization;
8+
using EventStore.Core.Authorization.AuthorizationPolicies;
89
using EventStore.Core.Bus;
910
using EventStore.Core.Data;
1011
using EventStore.Core.Messages;
@@ -41,10 +42,11 @@ public LegacyPolicyVerification(bool allowAnonymousEndpointAccess, bool allowAno
4142
_allowAnonymousEndpointAccess = allowAnonymousEndpointAccess;
4243
_allowAnonymousStreamAccess = allowAnonymousStreamAccess;
4344
_overrideAnonymousGossipEndpointAccess = overrideAnonymousGossipEndpointAccess;
44-
_authorizationProvider = new InternalAuthorizationProviderFactory([new LegacyPolicySelectorFactory(
45-
allowAnonymousEndpointAccess,
46-
allowAnonymousStreamAccess,
47-
overrideAnonymousGossipEndpointAccess).Create(_aclResponder, default)])
45+
_authorizationProvider = new InternalAuthorizationProviderFactory(
46+
new StaticAuthorizationPolicyRegistry([new LegacyPolicySelectorFactory(
47+
allowAnonymousEndpointAccess,
48+
allowAnonymousStreamAccess,
49+
overrideAnonymousGossipEndpointAccess).Create(_aclResponder)]))
4850
.Build();
4951
}
5052

src/EventStore.Core.Tests/ClientOperations/specification_with_bare_vnode.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using EventStore.Core.Authentication;
66
using EventStore.Core.Authentication.InternalAuthentication;
77
using EventStore.Core.Authorization;
8+
using EventStore.Core.Authorization.AuthorizationPolicies;
89
using EventStore.Core.Bus;
910
using EventStore.Core.Certificates;
1011
using EventStore.Core.Messaging;
@@ -28,11 +29,11 @@ public void CreateTestNode()
2829
_node = new ClusterVNode<TStreamId>(options, logFormatFactory,
2930
new AuthenticationProviderFactory(
3031
c => new InternalAuthenticationProviderFactory(c, options.DefaultUser)),
31-
new AuthorizationProviderFactory(c => new InternalAuthorizationProviderFactory([
32-
new LegacyPolicySelectorFactory(
33-
options.Application.AllowAnonymousEndpointAccess,
34-
options.Application.AllowAnonymousStreamAccess,
35-
options.Application.OverrideAnonymousEndpointAccessForGossip).Create(c.MainQueue, default)])),
32+
new AuthorizationProviderFactory(c => new InternalAuthorizationProviderFactory(
33+
new StaticAuthorizationPolicyRegistry([new LegacyPolicySelectorFactory(
34+
options.Application.AllowAnonymousEndpointAccess,
35+
options.Application.AllowAnonymousStreamAccess,
36+
options.Application.OverrideAnonymousEndpointAccessForGossip).Create(c.MainQueue)]))),
3637
certificateProvider: new OptionsCertificateProvider());
3738

3839
var builder = WebApplication.CreateBuilder();

src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using EventStore.Core.Authentication;
1212
using EventStore.Core.Authentication.InternalAuthentication;
1313
using EventStore.Core.Authorization;
14+
using EventStore.Core.Authorization.AuthorizationPolicies;
1415
using EventStore.Core.Bus;
1516
using EventStore.Core.Certificates;
1617
using EventStore.Core.Data;
@@ -191,11 +192,12 @@ public MiniClusterNode(string pathname, int debugIndex, IPEndPoint internalTcp,
191192
components =>
192193
new InternalAuthenticationProviderFactory(components, options.DefaultUser)),
193194
new AuthorizationProviderFactory(components =>
194-
new InternalAuthorizationProviderFactory([new LegacyPolicySelectorFactory(
195-
options.Application.AllowAnonymousEndpointAccess,
196-
options.Application.AllowAnonymousStreamAccess,
197-
options.Application.OverrideAnonymousEndpointAccessForGossip).Create(components.MainQueue, default)])),
198-
Array.Empty<IPersistentSubscriptionConsumerStrategyFactory>(),
195+
new InternalAuthorizationProviderFactory(
196+
new StaticAuthorizationPolicyRegistry([new LegacyPolicySelectorFactory(
197+
options.Application.AllowAnonymousEndpointAccess,
198+
options.Application.AllowAnonymousStreamAccess,
199+
options.Application.OverrideAnonymousEndpointAccessForGossip).Create(components.MainQueue)]))),
200+
[],
199201
new OptionsCertificateProvider(),
200202
configuration: inMemConf,
201203
expiryStrategy,

src/EventStore.Core.Tests/Helpers/MiniNode.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using EventStore.Core.Authentication;
1212
using EventStore.Core.Authentication.InternalAuthentication;
1313
using EventStore.Core.Authorization;
14+
using EventStore.Core.Authorization.AuthorizationPolicies;
1415
using EventStore.Core.Bus;
1516
using EventStore.Core.Certificates;
1617
using EventStore.Core.Configuration.Sources;
@@ -215,11 +216,11 @@ public MiniNode(string pathname,
215216
c,
216217
options.DefaultUser)),
217218
new AuthorizationProviderFactory(
218-
c => authorizationProviderFactory ?? new InternalAuthorizationProviderFactory([
219-
new LegacyPolicySelectorFactory(
220-
options.Application.AllowAnonymousEndpointAccess,
221-
options.Application.AllowAnonymousStreamAccess,
222-
options.Application.OverrideAnonymousEndpointAccessForGossip).Create(c.MainQueue, default)])),
219+
c => authorizationProviderFactory ?? new InternalAuthorizationProviderFactory(
220+
new StaticAuthorizationPolicyRegistry([new LegacyPolicySelectorFactory(
221+
options.Application.AllowAnonymousEndpointAccess,
222+
options.Application.AllowAnonymousStreamAccess,
223+
options.Application.OverrideAnonymousEndpointAccessForGossip).Create(c.MainQueue)]))),
223224
expiryStrategy: expiryStrategy,
224225
certificateProvider: new OptionsCertificateProvider(),
225226
configuration: inMemConf,

0 commit comments

Comments
 (0)