From 43654d47a87ae4e296c201296ffe6db8609a95cb Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 3 Dec 2024 14:41:27 -0600 Subject: [PATCH] Reload license when file or options change --- .../Licensing/v2/LicenseAccessor.cs | 41 +++++++++++++++++-- .../Licensing/v2/LicenseAccessorTests.cs | 39 ++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/IdentityServer/Licensing/v2/LicenseAccessor.cs b/src/IdentityServer/Licensing/v2/LicenseAccessor.cs index 1832d3ee2..a6c09b97f 100644 --- a/src/IdentityServer/Licensing/v2/LicenseAccessor.cs +++ b/src/IdentityServer/Licensing/v2/LicenseAccessor.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using Duende.IdentityServer.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -18,7 +19,7 @@ namespace Duende.IdentityServer.Licensing.v2; /// /// Loads the license from configuration or a file, and validates its contents. /// -internal class LicenseAccessor(IdentityServerOptions options, ILogger logger) : ILicenseAccessor +internal class LicenseAccessor : ILicenseAccessor { static readonly string[] LicenseFileNames = [ @@ -26,8 +27,42 @@ internal class LicenseAccessor(IdentityServerOptions options, ILogger _options; + private readonly ILogger _logger; + private readonly FileSystemWatcher _watcher; + + public LicenseAccessor(IOptionsMonitor options, ILogger logger) + { + _options = options; + _logger = logger; + + // If the options change, discard the license so that we will read updates + _options.OnChange(opt => + { + _license = null; + }); + + // If the license key file changes in any way, discard the license so that we will read updates + _watcher = new FileSystemWatcher(Directory.GetCurrentDirectory()); + _watcher.Filter = "*.key"; + _watcher.Changed += DiscardLicense; + _watcher.Deleted += DiscardLicense; + _watcher.Created += DiscardLicense; + _watcher.Renamed += DiscardLicense; + + _watcher.EnableRaisingEvents = true; + } + + + private void DiscardLicense(object sender, FileSystemEventArgs e) + { + _logger.LogDebug("License file change detected, license will be reloaded"); + _license = null; + } + private License? _license; private readonly object _lock = new(); + public License Current => _license ??= Initialize(); private License Initialize() @@ -36,7 +71,7 @@ private License Initialize() { if (_license != null) return _license; - var key = options.LicenseKey; + var key = _options.CurrentValue.LicenseKey; if (key == null) { key = LoadLicenseKeyFromFile(); @@ -96,7 +131,7 @@ private Claim[] ValidateKey(string licenseKey) var validateResult = handler.ValidateTokenAsync(licenseKey, parms).Result; if (!validateResult.IsValid) { - logger.LogCritical(validateResult.Exception, "Error validating the Duende software license key"); + _logger.LogCritical(validateResult.Exception, "Error validating the Duende software license key"); } return validateResult.ClaimsIdentity?.Claims.ToArray() ?? []; diff --git a/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs b/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs index 94b30cf7b..d9d8fd217 100644 --- a/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs +++ b/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs @@ -7,12 +7,43 @@ using Duende.IdentityServer.Configuration; using FluentAssertions; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; using Xunit; namespace IdentityServer.UnitTests.Licensing.v2; +public class TestOptionsMonitor : IOptionsMonitor +{ + private Action _listener; + + public TestOptionsMonitor(TOptions currentValue) + { + CurrentValue = currentValue; + } + + public TOptions CurrentValue { get; private set; } + + public TOptions Get(string name) + { + return CurrentValue; + } + + public void Set(TOptions value) + { + CurrentValue = value; + _listener.Invoke(value, null); + } + + public IDisposable OnChange(Action listener) + { + _listener = listener; + return null; + } +} + public class LicenseAccessorTests { + private readonly TestOptionsMonitor _optionsMonitor; private readonly IdentityServerOptions _options; private readonly LicenseAccessor _licenseAccessor; private readonly FakeLogger _logger; @@ -20,8 +51,9 @@ public class LicenseAccessorTests public LicenseAccessorTests() { _options = new IdentityServerOptions(); + _optionsMonitor = new TestOptionsMonitor(_options); _logger = new FakeLogger(); - _licenseAccessor = new LicenseAccessor(_options, _logger); + _licenseAccessor = new LicenseAccessor(_optionsMonitor, _logger); } [Theory] @@ -29,6 +61,7 @@ public LicenseAccessorTests() internal void license_set_in_options_is_parsed_correctly(int serialNumber, LicenseEdition edition, bool isRedistribution, string contact, bool addDynamicProviders, bool addKeyManagement, string key) { _options.LicenseKey = key; + _optionsMonitor.Set(_options); var l = _licenseAccessor.Current; @@ -55,7 +88,6 @@ internal void license_set_in_options_is_parsed_correctly(int serialNumber, Licen _licenseAccessor.Current.IsEnabled(LicenseFeature.ServerSideSessions).Should().Be(businessFeaturesEnabled); } - public static IEnumerable LicenseTestCases() => [ // Order of parameters is: int serialNumber, LicenseEdition edition, bool isRedistribution, string contact, string key @@ -76,13 +108,12 @@ public static IEnumerable LicenseTestCases() => [6681, LicenseEdition.Business, false, "joe@duendesoftware.com", true, false, "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzMwNDE5MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJCdXNpbmVzcyIsImlkIjoiNjY4MSIsImZlYXR1cmUiOiJkeW5hbWljX3Byb3ZpZGVycyJ9.HeCNt4O1cXsw4Ujkn2W_sDRmWUDstYtLPQ7UhYvneUgxed7auFyroBJojkwh9RwflWD1HphHYx4KRuZML_OO0BYzGr865gWI55x6KxHM5mxY5hpVJMTLottSgIv-hyXdNxTWCxP1jluzs1b4JgWmXnU83AuRtAenMpZpZcOY7Pldkd84JA1BXE5gEM6v2U8HCTgydY1QmTd_RjYlicGqmDOkKALiHOxREyXLsRgy4pmQfG6gs99heXdzs2k4jRLLXsTFHP7UxupRTYDPCgXT19ub6l4KG95rPBSMV_vXEwydcFGJe1uFQdd1btUSVe50XX1hmZx4P4SymlX0iuimMg"], [6680, LicenseEdition.Starter, false, "joe@duendesoftware.com", false, true, "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzMwNDE5MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI2NjgwIiwiZmVhdHVyZSI6ImtleV9tYW5hZ2VtZW50In0.kmArT0vjFE4nhRNg_kchOh_uklaqm3KeworQ9up_4jIBOinbZtVv3NkXtJoHX_lzjs1ftp0eNMSyGg6E29GR7ZZ2hx3SQdQrSdrH4v_sNSFcRZrwzipXBkANssH-0hMQ0s3kdfXdwfmN_8IfCkPCugeMemwUWwbC7QHBdCa6Fr7ZExuMNLpml932D72LMzhlLf780BSic9PKn6odvzGikYK9e2WhYL1zL0REdNHzgwrrUZHesZF98u-gel7skS1Frg6cBcPl_QSSP5KhxmfdPw0b2FUM_B0Tpi-gN54efz0stzccjr9PgcpAfXO82y3vOBB7f44cdv6DG67YwAvv0A"] ]; - - [Fact] public void keys_that_cannot_be_parsed_are_treated_the_same_as_an_absent_license() { _options.LicenseKey = "invalid key"; + _optionsMonitor.Set(_options); _licenseAccessor.Current.IsConfigured.Should().BeFalse(); _logger.Collector.GetSnapshot().Should().Contain(r => r.Message == "Error validating the Duende software license key");