Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jobs:
shell: pwsh
- name: Report Test Results
uses: dorny/test-reporter@v1
# Publishing the report needs checks:write, which GitHub forces to read-only
# for pull_request runs from forks. Don't let that fail the build; the test
# step itself already reflects pass/fail.
continue-on-error: true
if: success() || failure()
with:
name: Test Results (Linux)
Expand All @@ -55,6 +59,9 @@ jobs:
with:
fetch-depth: 0
- name: Azure Login via OIDC
# Only needed to sign and publish packages on main. Skipped on PRs,
# where the Azure secrets aren't available (and are empty on forks).
if: github.ref == 'refs/heads/main'
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
Expand All @@ -74,12 +81,17 @@ jobs:
shell: pwsh
- name: Report Test Results
uses: dorny/test-reporter@v1
# See note on the Linux job: report publishing is best-effort and must
# never fail the build (forked-PR tokens are read-only).
continue-on-error: true
if: success() || failure()
with:
name: Test Results (Windows)
path: artifacts/**/*.trx
reporter: dotnet-trx
- name: Sign packages
# Signing uses the Azure Key Vault login above; only runs on main.
if: github.ref == 'refs/heads/main'
run: |-
foreach ($f in Get-ChildItem "./artifacts" -Filter "*.nupkg") {
NuGetKeyVaultSignTool sign $f.FullName --file-digest sha256 --timestamp-rfc3161 http://timestamp.digicert.com --azure-key-vault-managed-identity --azure-key-vault-url ${{ secrets.AZURE_KEYVAULT_URI }} --azure-key-vault-certificate ${{ secrets.CODESIGN_CERT_NAME }}
Expand Down
15 changes: 15 additions & 0 deletions docs/source/License-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ var mapperConfiguration = new MapperConfiguration(cfg => {

You can obtain a valid license from the [AutoMapper website](https://automapper.io).

### Auto-Discovery via Environment Variables

If no license key is set in code, AutoMapper looks for one in environment variables. This is convenient for containerized and cloud environments, and for enterprises that share a single key across many services without code changes:

- `AUTOMAPPER_LICENSE_KEY` – the AutoMapper-specific license key.
- `LUCKYPENNY_LICENSE_KEY` – a shared key usable across Lucky Penny products (for example, [MediatR](https://github.com/LuckyPennySoftware/MediatR) reads the same variable). Because it is shared, the key must be for a license that includes AutoMapper (a `Bundle` or AutoMapper edition); a MediatR-only license will not validate here.

The license key is resolved in the following order of precedence, using the first value found:

1. An explicit value set in code (`cfg.LicenseKey = "..."`).
2. The `AUTOMAPPER_LICENSE_KEY` environment variable.
3. The `LUCKYPENNY_LICENSE_KEY` environment variable.

No code change is required when using an environment variable—just register AutoMapper as usual without setting `LicenseKey`.

### License Enforcement

Licensing is enforced via log messages at various levels:
Expand Down
15 changes: 14 additions & 1 deletion src/AutoMapper/Licensing/LicenseAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ namespace AutoMapper.Licensing;

internal class LicenseAccessor
{
internal const string AutoMapperLicenseKeyEnvVariable = "AUTOMAPPER_LICENSE_KEY";
internal const string SharedLicenseKeyEnvVariable = "LUCKYPENNY_LICENSE_KEY";

private readonly IGlobalConfiguration _configuration;
private readonly ILogger _logger;

Expand All @@ -32,7 +35,7 @@ private License Initialize()
return _license;
}

var key = _configuration.LicenseKey;
var key = ResolveLicenseKey(_configuration.LicenseKey);
if (key == null)
{
return new License();
Expand All @@ -45,6 +48,16 @@ private License Initialize()
}
}

/// <summary>
/// Resolves the license key from, in order of precedence: the explicitly configured value,
/// the product-specific <c>AUTOMAPPER_LICENSE_KEY</c> environment variable, then the shared
/// <c>LUCKYPENNY_LICENSE_KEY</c> environment variable (usable across Lucky Penny products).
/// </summary>
internal static string ResolveLicenseKey(string explicitKey) =>
explicitKey
?? Environment.GetEnvironmentVariable(AutoMapperLicenseKeyEnvVariable)
?? Environment.GetEnvironmentVariable(SharedLicenseKeyEnvVariable);

private Claim[] ValidateKey(string licenseKey)
{
try
Expand Down
69 changes: 69 additions & 0 deletions src/UnitTests/Licensing/LicenseKeyEnvironmentVariableTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using AutoMapper.Licensing;

namespace AutoMapper.UnitTests.Licensing;

// Mutates process-global environment variables, so it must not run alongside
// other tests that read them. Disable parallelization for this class.
[Collection(nameof(LicenseKeyEnvironmentVariableTests))]
[CollectionDefinition(nameof(LicenseKeyEnvironmentVariableTests), DisableParallelization = true)]
public class LicenseKeyEnvironmentVariableTests
{
private const string AutoMapperEnvVar = LicenseAccessor.AutoMapperLicenseKeyEnvVariable;
private const string SharedEnvVar = LicenseAccessor.SharedLicenseKeyEnvVariable;

[Fact]
public void ExplicitKey_TakesPrecedence_OverBothEnvironmentVariables()
{
const string explicitKey = "explicit-license-key";
WithEnvironment(autoMapper: "env-automapper-key", shared: "env-shared-key", () =>
LicenseAccessor.ResolveLicenseKey(explicitKey).ShouldBe(explicitKey));
}

[Fact]
public void AutoMapperEnvironmentVariable_Used_WhenNoExplicitKey()
{
const string autoMapperKey = "env-automapper-key";
WithEnvironment(autoMapper: autoMapperKey, shared: null, () =>
LicenseAccessor.ResolveLicenseKey(null).ShouldBe(autoMapperKey));
}

[Fact]
public void SharedEnvironmentVariable_Used_WhenOnlyItIsSet()
{
const string sharedKey = "env-shared-key";
WithEnvironment(autoMapper: null, shared: sharedKey, () =>
LicenseAccessor.ResolveLicenseKey(null).ShouldBe(sharedKey));
}

[Fact]
public void AutoMapperEnvironmentVariable_TakesPrecedence_OverSharedEnvironmentVariable()
{
const string autoMapperKey = "env-automapper-key";
WithEnvironment(autoMapper: autoMapperKey, shared: "env-shared-key", () =>
LicenseAccessor.ResolveLicenseKey(null).ShouldBe(autoMapperKey));
}

[Fact]
public void ReturnsNull_WhenNothingIsSet()
{
WithEnvironment(autoMapper: null, shared: null, () =>
LicenseAccessor.ResolveLicenseKey(null).ShouldBeNull());
}

private static void WithEnvironment(string autoMapper, string shared, Action assert)
{
var originalAutoMapper = Environment.GetEnvironmentVariable(AutoMapperEnvVar);
var originalShared = Environment.GetEnvironmentVariable(SharedEnvVar);
try
{
Environment.SetEnvironmentVariable(AutoMapperEnvVar, autoMapper);
Environment.SetEnvironmentVariable(SharedEnvVar, shared);
assert();
}
finally
{
Environment.SetEnvironmentVariable(AutoMapperEnvVar, originalAutoMapper);
Environment.SetEnvironmentVariable(SharedEnvVar, originalShared);
}
}
}
Loading