diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj index 4474733c0f..2e1bcd97bf 100644 --- a/src/AutoMapper/AutoMapper.csproj +++ b/src/AutoMapper/AutoMapper.csproj @@ -60,6 +60,20 @@ --> + + + + + + + $(GitCommitDate) + + + + + + + diff --git a/src/AutoMapper/Licensing/BuildInfo.cs b/src/AutoMapper/Licensing/BuildInfo.cs new file mode 100644 index 0000000000..17e52a364f --- /dev/null +++ b/src/AutoMapper/Licensing/BuildInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace AutoMapper.Licensing; + +internal static class BuildInfo +{ + public static DateTimeOffset? BuildDate { get; } = GetBuildDate(); + + private static DateTimeOffset? GetBuildDate() + { + var assembly = typeof(BuildInfo).Assembly; + + var buildDateAttribute = assembly + .GetCustomAttributes() + .FirstOrDefault(a => a.Key == "BuildDateUtc"); + + if (buildDateAttribute?.Value != null && + DateTimeOffset.TryParse(buildDateAttribute.Value, out var buildDate)) + { + return buildDate; + } + + return null; + } +} + diff --git a/src/AutoMapper/Licensing/License.cs b/src/AutoMapper/Licensing/License.cs index 2b62092b1d..2fd51d4455 100644 --- a/src/AutoMapper/Licensing/License.cs +++ b/src/AutoMapper/Licensing/License.cs @@ -41,6 +41,9 @@ public License(ClaimsPrincipal claims) ProductType = productType; } + var perpetualValue = claims.FindFirst("perpetual")?.Value; + IsPerpetual = perpetualValue?.ToLowerInvariant() is "true" or "1"; + IsConfigured = AccountId != null && CustomerId != null && SubscriptionId != null @@ -57,6 +60,7 @@ public License(ClaimsPrincipal claims) public DateTimeOffset? ExpirationDate { get; } public Edition? Edition { get; } public ProductType? ProductType { get; } + public bool IsPerpetual { get; } public bool IsConfigured { get; } } \ No newline at end of file diff --git a/src/AutoMapper/Licensing/LicenseValidator.cs b/src/AutoMapper/Licensing/LicenseValidator.cs index 52366beaa5..485ed10e5e 100644 --- a/src/AutoMapper/Licensing/LicenseValidator.cs +++ b/src/AutoMapper/Licensing/LicenseValidator.cs @@ -5,10 +5,17 @@ namespace AutoMapper.Licensing; internal class LicenseValidator { private readonly ILogger _logger; + private readonly DateTimeOffset? _buildDate; public LicenseValidator(ILoggerFactory loggerFactory) + : this(loggerFactory, BuildInfo.BuildDate) + { + } + + public LicenseValidator(ILoggerFactory loggerFactory, DateTimeOffset? buildDate) { _logger = loggerFactory.CreateLogger("LuckyPennySoftware.AutoMapper.License"); + _buildDate = buildDate; } public void Validate(License license) @@ -31,7 +38,31 @@ public void Validate(License license) var diff = DateTime.UtcNow.Date.Subtract(license.ExpirationDate.Value.Date).TotalDays; if (diff > 0) { - errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); + // If perpetual, check if build date is before expiration + if (license.IsPerpetual && _buildDate.HasValue) + { + var buildDateDiff = _buildDate.Value.Date.Subtract(license.ExpirationDate.Value.Date).TotalDays; + if (buildDateDiff <= 0) + { + _logger.LogInformation( + "Your license for the Lucky Penny software AutoMapper expired {expiredDaysAgo} days ago, but perpetual licensing is active because the build date ({buildDate:O}) is before the license expiration date ({licenseExpiration:O}).", + diff, _buildDate, license.ExpirationDate); + // Don't add to errors - perpetual fallback applies + } + else + { + errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); + } + } + else + { + if (license.IsPerpetual) + { + _logger.LogWarning( + "Your license for the Lucky Penny software AutoMapper has the perpetual flag set, but the build date could not be determined. Perpetual licensing is unavailable."); + } + errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); + } } if (license.ProductType.Value != ProductType.AutoMapper diff --git a/src/UnitTests/Licensing/LicenseValidatorTests.cs b/src/UnitTests/Licensing/LicenseValidatorTests.cs index 95ab06b3e1..496d398484 100644 --- a/src/UnitTests/Licensing/LicenseValidatorTests.cs +++ b/src/UnitTests/Licensing/LicenseValidatorTests.cs @@ -110,4 +110,119 @@ public void Should_return_invalid_when_expired() logMessages .ShouldContain(log => log.Level == LogLevel.Error); } + + [Fact] + public void Should_allow_perpetual_license_when_build_date_before_expiration() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var buildDate = DateTimeOffset.UtcNow.AddDays(-30); + var licenseValidator = new LicenseValidator(factory, buildDate); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)), + new Claim("perpetual", "true")); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldNotContain(log => log.Level == LogLevel.Error); + logMessages.ShouldContain(log => log.Level == LogLevel.Information && + log.Message.Contains("perpetual")); + } + + [Fact] + public void Should_reject_perpetual_license_when_build_date_after_expiration() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var buildDate = DateTimeOffset.UtcNow.AddDays(-1); // Build date in past, after expiration + var licenseValidator = new LicenseValidator(factory, buildDate); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-30).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)), + new Claim("perpetual", "true")); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldContain(log => log.Level == LogLevel.Error && + log.Message.Contains("expired")); + } + + [Fact] + public void Should_warn_and_error_when_perpetual_but_build_date_is_null() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory, buildDate: null); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-10).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)), + new Claim("perpetual", "true")); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldContain(log => log.Level == LogLevel.Warning && log.Message.Contains("perpetual")); + logMessages.ShouldContain(log => log.Level == LogLevel.Error && log.Message.Contains("expired")); + } + + [Fact] + public void Should_handle_missing_perpetual_claim() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Community)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.Bundle))); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeFalse(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldNotContain(log => log.Level == LogLevel.Error + || log.Level == LogLevel.Warning + || log.Level == LogLevel.Critical); + } } \ No newline at end of file