From 77da232bba98a930fca3e54b46007cd6454dbfbd Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 6 Sep 2026 04:32:01 +0200 Subject: [PATCH] fix(identity): select active role store for remediation --- ...cationRoleDeletionDependencyContributor.cs | 4 +- ...nRoleDeletionDependencyContributorTests.cs | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs b/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs index 7e842d51e..71c9fa3fc 100644 --- a/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs +++ b/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs @@ -147,7 +147,9 @@ public sealed class ExternalAuthenticationRoleDeletionDependencyContributor( if (request.SelectedReferences is not null && removesLastDefaultRole) { - var roleStore = roleStores.SingleOrDefault(); + // Match the default DI container's direct-service semantics: when persistence + // replaces the in-memory store, the last registration is the active store. + var roleStore = roleStores.LastOrDefault(); if (roleStore is null) return new RoleReferenceRemovalResult.Failed("replacement_role_unavailable_or_unauthorized", changedOwnerIds); var replacement = await roleStore.FindAsync(new() { Id = request.ReplacementRoleId }, cancellationToken); diff --git a/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs b/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs index bd015e002..0743bba4b 100644 --- a/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs +++ b/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs @@ -138,6 +138,47 @@ public class ExternalAuthenticationRoleDeletionDependencyContributorTests Assert.Equal(["replacement-role"], updated.UnlinkedPolicy!.Settings.GetProperty("defaultRoleIds").EnumerateArray().Select(x => x.GetString()!).ToArray()); } + [Fact] + public async Task UsesTheActiveRoleStoreWhenPersistenceReplacesTheDefaultStore() + { + var databaseConnection = Connection( + "database", + new PolicySelection( + CreateUserUnlinkedIdentityPolicy.PolicyType, + 1, + JsonSerializer.SerializeToElement(new { defaultRoleIds = new[] { "workflow-user" } }))); + var connectionStore = new InMemoryIdentityProviderConnectionStore(); + Assert.IsType(await connectionStore.CreateAsync(databaseConnection)); + var replacedStore = new MemoryRoleStore(new MemoryStore(), TestTenantAccessor.Default); + var activeStore = new MemoryRoleStore(new MemoryStore(), TestTenantAccessor.Default); + await activeStore.SaveAsync(new Role { Id = "workflow-user", Name = "Workflow user", Permissions = [] }); + await activeStore.SaveAsync(new Role { Id = "replacement-role", Name = "Replacement role", Permissions = [] }); + var roleAuthorizationService = new RoleAuthorizationService(new StoreBasedRoleProvider(activeStore), new PermissionEvaluator()); + var services = new ServiceCollection().BuildServiceProvider(); + var contributor = new ExternalAuthenticationRoleDeletionDependencyContributor( + connectionStore, + new MutableOptionsMonitor(new ExternalAuthenticationOptions()), + [roleAuthorizationService], + [replacedStore, activeStore], + new InMemoryConnectionRegistryVersionStore(), + new ConnectionRevisionCalculator(), + new ExternalAuthenticationSecurityNotifier(services), + new PermissionEvaluator()); + var snapshot = await contributor.InspectAsync("workflow-user"); + var request = new RoleReferenceRemovalRequest( + "workflow-user", + Administrator(), + snapshot.Version, + snapshot.Dependencies) + { + SelectedReferences = [new RoleDeletionReferenceSelection(ExternalAuthenticationRoleDeletionDependencyContributor.SourceName, databaseConnection.Id)], + ReplacementRoleId = "replacement-role" + }; + + Assert.IsType(await contributor.ValidateRemovalAsync(request)); + Assert.IsType(await contributor.RemoveEditableReferencesAsync(request)); + } + [Fact] public async Task RejectsMissingReplacementForSelectedFinalDefaultRole() {