Skip to content

Commit 7dbd2cb

Browse files
Separate licence monitoring mechanism into its own class
Going forward Plugins aren't the only thing that might need licensing, built in features may also.
1 parent c9e60bd commit 7dbd2cb

File tree

8 files changed

+195
-54
lines changed

8 files changed

+195
-54
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace EventStore.Plugins;
2+
3+
public class LicenseException(string featureName, Exception? inner = null) : Exception(
4+
$"A license is required to use the {featureName} feature, but was not found. " +
5+
"Please obtain a license or disable the feature.",
6+
inner
7+
) {
8+
public string FeatureName { get; } = featureName;
9+
}
10+
11+
public class LicenseEntitlementException(string featureName, string entitlement) : Exception(
12+
$"{featureName} feature requires the {entitlement} entitlement. Please contact EventStore support.") {
13+
public string FeatureName { get; } = featureName;
14+
public string MissingEntitlement { get; } = entitlement;
15+
}

src/EventStore.Plugins/Licensing/License.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ public async Task<bool> ValidateAsync(string publicKey) {
1414
return result.IsValid;
1515
}
1616

17+
public async Task<bool> TryValidateAsync(string publicKey) {
18+
try {
19+
return await ValidateAsync(publicKey);
20+
} catch {
21+
return false;
22+
}
23+
}
24+
1725
public bool HasEntitlements(string[] entitlements, [MaybeNullWhen(true)] out string missing) {
1826
foreach (var entitlement in entitlements) {
1927
if (!HasEntitlement(entitlement)) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace EventStore.Plugins.Licensing;
2+
3+
public static class LicenseConstants {
4+
public const string LicensePublicKey = "MEgCQQDGtRXIWmeJqkdpQryJdKBFVvLaMNHFkDcVXSoaDzg1ahrtCrAgwYpARAvGyFs0bcwYJZaZSt9aNwpgkAPOPQM5AgMBAAE=";
5+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace EventStore.Plugins.Licensing;
4+
5+
public static class LicenseMonitor {
6+
// the EULA prevents tampering with the license mechanism. we make the license mechanism
7+
// robust enough that circumventing it requires intentional tampering.
8+
public static async Task<IDisposable> MonitorAsync(
9+
string featureName,
10+
string[] requiredEntitlements,
11+
ILicenseService licenseService,
12+
ILogger logger,
13+
string licensePublicKey = LicenseConstants.LicensePublicKey,
14+
Action<int>? onCriticalError = null) {
15+
16+
onCriticalError ??= Environment.Exit;
17+
18+
// authenticate the license service itself so that we can trust it to
19+
// 1. send us any licences at all
20+
// 2. respect our decision to reject licences
21+
var authentic = await licenseService.SelfLicense.TryValidateAsync(licensePublicKey);
22+
if (!authentic) {
23+
// this should never happen, but could if we end up with some unknown LicenseService.
24+
logger.LogCritical("LicenseService could not be authenticated");
25+
onCriticalError(11);
26+
}
27+
28+
// authenticate the licenses that the license service sends us
29+
return licenseService.Licenses.Subscribe(
30+
onNext: async license => {
31+
if (await license.TryValidateAsync(licensePublicKey)) {
32+
// got an authentic license. check required entitlements
33+
if (license.HasEntitlement("ALL"))
34+
return;
35+
36+
if (!license.HasEntitlements(requiredEntitlements, out var missing)) {
37+
licenseService.RejectLicense(new LicenseEntitlementException(featureName, missing));
38+
}
39+
} else {
40+
// this should never happen
41+
logger.LogCritical("ESDB License was not valid");
42+
licenseService.RejectLicense(new LicenseException(featureName, new Exception("ESDB License was not valid")));
43+
onCriticalError(12);
44+
}
45+
},
46+
onError: ex => {
47+
licenseService.RejectLicense(new LicenseException(featureName, ex));
48+
});
49+
}
50+
}

src/EventStore.Plugins/Plugin.cs

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -136,43 +136,14 @@ void IPlugableComponent.ConfigureApplication(IApplicationBuilder app, IConfigura
136136

137137
if (Enabled && LicensePublicKey is not null) {
138138
// the plugin is enabled and requires a license
139-
// the EULA prevents tampering with the license mechanism. we make the license mechanism
140-
// robust enough that circumventing it requires intentional tampering.
141139
var licenseService = app.ApplicationServices.GetRequiredService<ILicenseService>();
142140

143-
// authenticate the license service itself so that we can trust it to
144-
// 1. send us any licences at all
145-
// 2. respect our decision to reject licences
146-
Task.Run(async () => {
147-
var authentic = await licenseService.SelfLicense.ValidateAsync(LicensePublicKey);
148-
if (!authentic) {
149-
// this should never happen, but could if we end up with some unknown LicenseService.
150-
logger.LogCritical("LicenseService could not be authenticated");
151-
Environment.Exit(11);
152-
}
153-
});
154-
155-
// authenticate the licenses that the license service sends us
156-
licenseService.Licenses.Subscribe(
157-
onNext: async license => {
158-
if (await license.ValidateAsync(LicensePublicKey)) {
159-
// got an authentic license. check required entitlements
160-
if (license.HasEntitlement("ALL"))
161-
return;
162-
163-
if (!license.HasEntitlements(RequiredEntitlements ?? [], out var missing)) {
164-
licenseService.RejectLicense(new PluginLicenseEntitlementException(Name, missing));
165-
}
166-
} else {
167-
// this should never happen
168-
logger.LogCritical("ESDB License was not valid");
169-
licenseService.RejectLicense(new PluginLicenseException(Name, new Exception("ESDB License was not valid")));
170-
Environment.Exit(12);
171-
}
172-
},
173-
onError: ex => {
174-
licenseService.RejectLicense(new PluginLicenseException(Name, ex));
175-
});
141+
_ = LicenseMonitor.MonitorAsync(
142+
Name,
143+
RequiredEntitlements ?? [],
144+
licenseService,
145+
logger,
146+
LicensePublicKey);
176147
}
177148

178149
// there is still a chance to disable the plugin when configuring the application

src/EventStore.Plugins/PluginLicenseException.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using EventStore.Plugins.Licensing;
2+
using Microsoft.Extensions.Logging.Testing;
3+
4+
namespace EventStore.Plugins.Tests.Licensing;
5+
6+
public class LicenseMonitorTests {
7+
[Fact]
8+
public async Task valid_license_with_correct_entitlements() {
9+
var licenseService = new PluginBaseTests.FakeLicenseService(
10+
createLicense: true,
11+
entitlements: ["MY_ENTITLEMENT"]);
12+
13+
var criticalError = false;
14+
15+
using var subscription = await LicenseMonitor.MonitorAsync(
16+
featureName: "TestFeature",
17+
requiredEntitlements: ["MY_ENTITLEMENT"],
18+
licenseService: licenseService,
19+
logger: new FakeLogger(),
20+
licensePublicKey: licenseService.PublicKey,
21+
onCriticalError: _ => criticalError = true);
22+
23+
licenseService.RejectionException.Should().BeNull();
24+
criticalError.Should().BeFalse();
25+
}
26+
27+
[Fact]
28+
public async Task valid_license_with_all_entitlement() {
29+
var licenseService = new PluginBaseTests.FakeLicenseService(
30+
createLicense: true,
31+
entitlements: ["ALL"]);
32+
33+
var criticalError = false;
34+
35+
using var subscription = await LicenseMonitor.MonitorAsync(
36+
featureName: "TestFeature",
37+
requiredEntitlements: ["MY_ENTITLEMENT"],
38+
licenseService: licenseService,
39+
logger: new FakeLogger(),
40+
licensePublicKey: licenseService.PublicKey,
41+
onCriticalError: _ => criticalError = true);
42+
43+
licenseService.RejectionException.Should().BeNull();
44+
criticalError.Should().BeFalse();
45+
}
46+
47+
[Fact]
48+
public async Task valid_license_with_missing_entitlement() {
49+
var licenseService = new PluginBaseTests.FakeLicenseService(
50+
createLicense: true,
51+
entitlements: []);
52+
53+
var criticalError = false;
54+
55+
using var subscription = await LicenseMonitor.MonitorAsync(
56+
featureName: "TestFeature",
57+
requiredEntitlements: ["MY_ENTITLEMENT"],
58+
licenseService: licenseService,
59+
logger: new FakeLogger(),
60+
licensePublicKey: licenseService.PublicKey,
61+
onCriticalError: _ => criticalError = true);
62+
63+
licenseService.RejectionException.Should().BeOfType<LicenseEntitlementException>()
64+
.Which.MissingEntitlement.Should().Be("MY_ENTITLEMENT");
65+
criticalError.Should().BeFalse();
66+
}
67+
68+
[Fact]
69+
public async Task no_license() {
70+
var licenseService = new PluginBaseTests.FakeLicenseService(
71+
createLicense: false);
72+
73+
var criticalError = false;
74+
75+
using var subscription = await LicenseMonitor.MonitorAsync(
76+
featureName: "TestFeature",
77+
requiredEntitlements: [],
78+
licenseService: licenseService,
79+
logger: new FakeLogger(),
80+
licensePublicKey: licenseService.PublicKey,
81+
onCriticalError: _ => criticalError = true);
82+
83+
licenseService.RejectionException.Should().BeOfType<LicenseException>();
84+
criticalError.Should().BeFalse();
85+
}
86+
87+
[Fact]
88+
public async Task license_is_not_valid() {
89+
var licenseService = new PluginBaseTests.FakeLicenseService(
90+
createLicense: true,
91+
entitlements: []);
92+
93+
var criticalError = false;
94+
95+
using var subscription = await LicenseMonitor.MonitorAsync(
96+
featureName: "TestFeature",
97+
requiredEntitlements: [],
98+
licenseService: licenseService,
99+
logger: new FakeLogger(),
100+
licensePublicKey: "a_different_public_key",
101+
onCriticalError: _ => criticalError = true);
102+
103+
licenseService.RejectionException.Should().BeOfType<LicenseException>();
104+
criticalError.Should().BeTrue();
105+
}
106+
}

test/EventStore.Plugins.Tests/PluginBaseTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ public void commercial_plugin_is_disabled_when_licence_is_missing() {
9393
plugin.ConfigureApplication(app, app.Configuration);
9494

9595
// Assert
96-
licenseService.RejectionException.Should().BeOfType<PluginLicenseException>().Which
97-
.PluginName.Should().Be(plugin.Name);
96+
licenseService.RejectionException.Should().BeOfType<LicenseException>().Which
97+
.FeatureName.Should().Be(plugin.Name);
9898
}
9999

100100
[Fact]
@@ -119,8 +119,8 @@ public void commercial_plugin_is_disabled_when_licence_is_missing_entitlement()
119119
plugin.ConfigureApplication(app, app.Configuration);
120120

121121
// Assert
122-
licenseService.RejectionException.Should().BeOfType<PluginLicenseEntitlementException>().Which
123-
.PluginName.Should().Be(plugin.Name);
122+
licenseService.RejectionException.Should().BeOfType<LicenseEntitlementException>().Which
123+
.FeatureName.Should().Be(plugin.Name);
124124
}
125125

126126
[Fact]
@@ -197,7 +197,7 @@ public void plugin_can_be_disabled_on_ConfigureApplication() {
197197
.WhoseValue.Should().BeEquivalentTo(false);
198198
}
199199

200-
class FakeLicenseService : ILicenseService {
200+
public class FakeLicenseService : ILicenseService {
201201
public FakeLicenseService(
202202
bool createLicense,
203203
params string[] entitlements) {

0 commit comments

Comments
 (0)