fix(identity): select active role store for remediation

This commit is contained in:
Sipke Schoorstra 2026-09-06 04:32:01 +02:00
parent f9d41ee85b
commit 77da232bba
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
2 changed files with 44 additions and 1 deletions

View file

@ -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);

View file

@ -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<ConnectionMutationResult.Created>(await connectionStore.CreateAsync(databaseConnection));
var replacedStore = new MemoryRoleStore(new MemoryStore<Role>(), TestTenantAccessor.Default);
var activeStore = new MemoryRoleStore(new MemoryStore<Role>(), 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<ExternalAuthenticationOptions>(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<RoleReferenceRemovalValidationResult.Valid>(await contributor.ValidateRemovalAsync(request));
Assert.IsType<RoleReferenceRemovalResult.Success>(await contributor.RemoveEditableReferencesAsync(request));
}
[Fact]
public async Task RejectsMissingReplacementForSelectedFinalDefaultRole()
{