* 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>
63 lines
3 KiB
C#
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));
|
|
}
|
|
}
|