elsa-core/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/HmacExternalAuthenticationHandleHasherTests.cs
Sipke Schoorstra 168a8c76f0
fix(auth): validate wildcard permission patterns and warn on deny-list stripping (#7997)
* fix(auth): validate wildcard permission patterns and warn on deny-list stripping

Permission.IsValidPattern rejects inert wildcard spellings (such as
"workflows*:delete") that parse but can never match. The grant boundary,
stored-permission, and external-authentication options validators reject them
at authoring time, and PermissionGrantValidator applies the same check to
incoming grants.

ExternalAuthenticationOptionsValidator now warns (never fails) when
DeniedPermissions is non-empty, because any non-empty deny list refuses every
wildcard grant that could reach a denied permission -- including the seeded
administrator role's "*". The validator takes an ILogger, and
AddExternalAuthenticationServices registers logging alongside its other
framework dependencies (TryAdd-based, so host logging configuration wins).
The operational consequence is recorded in the authorization-model migration
guide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(auth): report subtree grants whose verb nothing under them supports

'workflows/*:frobnicate' reached a non-empty subtree and was therefore
treated as resolved, so the startup audit stayed silent about a grant
that cannot authorize anything. Require at least one reached descriptor
to support a concrete verb; verb wildcards keep the reach-only check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 11:45:54 +02:00

63 lines
3 KiB
C#

using Elsa.ExternalAuthentication.Options;
using Elsa.ExternalAuthentication.Services;
using Elsa.ExternalAuthentication.Validation;
using Microsoft.Extensions.Logging.Abstractions;
namespace Elsa.ExternalAuthentication.UnitTests.Foundational;
public class HmacExternalAuthenticationHandleHasherTests
{
[Fact]
public void ConfiguredSharedKeyProducesStableHashesAcrossNodes()
{
var options = Microsoft.Extensions.Options.Options.Create(new ExternalAuthenticationOptions
{
HandleHashing = new ExternalAuthenticationHandleHashingOptions
{
SharedKeyBase64 = Convert.ToBase64String(Enumerable.Range(0, 32).Select(x => (byte)x).ToArray())
}
});
using var firstNode = new HmacExternalAuthenticationHandleHasher(options);
using var secondNode = new HmacExternalAuthenticationHandleHasher(options);
Assert.Equal(firstNode.Hash("opaque-handle"), secondNode.Hash("opaque-handle"));
Assert.Equal(firstNode.Hash("issuer\u001fsubject"), secondNode.Hash("issuer\u001fsubject"));
}
[Fact]
public void ProcessLocalFallbackDoesNotCreateAClusterWideKey()
{
using var firstNode = new HmacExternalAuthenticationHandleHasher();
using var secondNode = new HmacExternalAuthenticationHandleHasher();
Assert.NotEqual(firstNode.Hash("opaque-handle"), secondNode.Hash("opaque-handle"));
}
[Theory]
[InlineData("not-base64")]
[InlineData("c2hvcnQ=")]
public void ValidatorRejectsInvalidSharedKeys(string sharedKey)
{
var options = new ExternalAuthenticationOptions
{
HandleHashing = new ExternalAuthenticationHandleHashingOptions { SharedKeyBase64 = sharedKey }
};
var extensions = new ExternalAuthenticationExtensionOptions();
extensions.Registrations.Add(new(ExternalAuthenticationExtensionKind.Adapter, "oidc"));
extensions.Registrations.Add(new(ExternalAuthenticationExtensionKind.UnlinkedIdentityPolicy, "reject"));
extensions.Registrations.Add(new(ExternalAuthenticationExtensionKind.UnlinkedIdentityPolicy, "create-user"));
extensions.Registrations.Add(new(ExternalAuthenticationExtensionKind.PermissionGrantSource, "elsa-roles"));
extensions.Registrations.Add(new(ExternalAuthenticationExtensionKind.PermissionGrantSource, "claim-mapping"));
extensions.Registrations.Add(new(ExternalAuthenticationExtensionKind.PermissionGrantSource, "group-mapping"));
extensions.Registrations.Add(new(ExternalAuthenticationExtensionKind.PermissionGrantSource, "claim-pass-through"));
var validator = new ExternalAuthenticationOptionsValidator(Microsoft.Extensions.Options.Options.Create(extensions), NullLogger<ExternalAuthenticationOptionsValidator>.Instance);
var result = validator.Validate(null, options);
Assert.False(result.Succeeded);
Assert.Contains(result.Failures!, failure => failure.Contains("SharedKeyBase64", StringComparison.Ordinal));
}
}