Skip to content

Commit 83baf78

Browse files
Merge pull request #56 from EventStore/timothycoleman/separate-license-monitoring
[ESDB-181-2] Separate licence monitoring mechanism into its own class
2 parents c9e60bd + 7dbd2cb commit 83baf78

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)