Skip to content

Commit

Permalink
Reload license when file or options change
Browse files Browse the repository at this point in the history
  • Loading branch information
josephdecock committed Dec 3, 2024
1 parent 072bfdd commit 43654d4
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 7 deletions.
41 changes: 38 additions & 3 deletions src/IdentityServer/Licensing/v2/LicenseAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -18,16 +19,50 @@ namespace Duende.IdentityServer.Licensing.v2;
/// <summary>
/// Loads the license from configuration or a file, and validates its contents.
/// </summary>
internal class LicenseAccessor(IdentityServerOptions options, ILogger<LicenseAccessor> logger) : ILicenseAccessor
internal class LicenseAccessor : ILicenseAccessor
{
static readonly string[] LicenseFileNames =
[
"Duende_License.key",
"Duende_IdentityServer_License.key",
];

private IOptionsMonitor<IdentityServerOptions> _options;
private readonly ILogger<LicenseAccessor> _logger;
private readonly FileSystemWatcher _watcher;

public LicenseAccessor(IOptionsMonitor<IdentityServerOptions> options, ILogger<LicenseAccessor> 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()
Expand All @@ -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();
Expand Down Expand Up @@ -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() ?? [];
Expand Down
39 changes: 35 additions & 4 deletions test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,61 @@
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<TOptions> : IOptionsMonitor<TOptions>
{
private Action<TOptions, string> _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<TOptions, string> listener)
{
_listener = listener;
return null;
}
}

public class LicenseAccessorTests
{
private readonly TestOptionsMonitor<IdentityServerOptions> _optionsMonitor;
private readonly IdentityServerOptions _options;
private readonly LicenseAccessor _licenseAccessor;
private readonly FakeLogger<LicenseAccessor> _logger;

public LicenseAccessorTests()
{
_options = new IdentityServerOptions();
_optionsMonitor = new TestOptionsMonitor<IdentityServerOptions>(_options);
_logger = new FakeLogger<LicenseAccessor>();
_licenseAccessor = new LicenseAccessor(_options, _logger);
_licenseAccessor = new LicenseAccessor(_optionsMonitor, _logger);
}

[Theory]
[MemberData(nameof(LicenseTestCases))]
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;

Expand All @@ -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<object[]> LicenseTestCases() =>
[
// Order of parameters is: int serialNumber, LicenseEdition edition, bool isRedistribution, string contact, string key
Expand All @@ -76,13 +108,12 @@ public static IEnumerable<object[]> LicenseTestCases() =>
[6681, LicenseEdition.Business, false, "[email protected]", true, false, "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzMwNDE5MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJCdXNpbmVzcyIsImlkIjoiNjY4MSIsImZlYXR1cmUiOiJkeW5hbWljX3Byb3ZpZGVycyJ9.HeCNt4O1cXsw4Ujkn2W_sDRmWUDstYtLPQ7UhYvneUgxed7auFyroBJojkwh9RwflWD1HphHYx4KRuZML_OO0BYzGr865gWI55x6KxHM5mxY5hpVJMTLottSgIv-hyXdNxTWCxP1jluzs1b4JgWmXnU83AuRtAenMpZpZcOY7Pldkd84JA1BXE5gEM6v2U8HCTgydY1QmTd_RjYlicGqmDOkKALiHOxREyXLsRgy4pmQfG6gs99heXdzs2k4jRLLXsTFHP7UxupRTYDPCgXT19ub6l4KG95rPBSMV_vXEwydcFGJe1uFQdd1btUSVe50XX1hmZx4P4SymlX0iuimMg"],
[6680, LicenseEdition.Starter, false, "[email protected]", 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");
Expand Down

0 comments on commit 43654d4

Please sign in to comment.