From 62fcc302c7cd79110910b294c7ef628faf281e89 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 6 Sep 2026 03:54:35 +0200 Subject: [PATCH] Guard optional Identity role services --- ...cationRoleDeletionDependencyContributor.cs | 13 ++++++++++-- ...nRoleDeletionDependencyContributorTests.cs | 8 ++++---- ...nalAuthenticationServiceCollectionTests.cs | 20 +++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs b/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs index 63a6479aa..7e842d51e 100644 --- a/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs +++ b/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs @@ -20,8 +20,8 @@ namespace Elsa.ExternalAuthentication.Services; public sealed class ExternalAuthenticationRoleDeletionDependencyContributor( IIdentityProviderConnectionStore store, IOptionsMonitor options, - IRoleAuthorizationService roleAuthorizationService, - IRoleStore roleStore, + IEnumerable roleAuthorizationServices, + IEnumerable roleStores, IConnectionRegistryVersionStore registryVersions, ConnectionRevisionCalculator revisionCalculator, ExternalAuthenticationSecurityNotifier notifier, @@ -67,6 +67,9 @@ public sealed class ExternalAuthenticationRoleDeletionDependencyContributor( public async ValueTask ValidateRemovalAsync(RoleReferenceRemovalRequest request, CancellationToken cancellationToken = default) { + var roleAuthorizationService = roleAuthorizationServices.SingleOrDefault(); + if (roleAuthorizationService is null) + return new RoleReferenceRemovalValidationResult.Forbidden("role_authorization_unavailable"); if (!permissionEvaluator.HasPermission(request.Actor, ExternalAuthenticationResourcePermissions.Connections, CoreVerbs.Update) || !permissionEvaluator.HasPermission(request.Actor, ExternalAuthenticationResourcePermissions.Policies, CoreVerbs.Update) || !permissionEvaluator.HasPermission(request.Actor, ExternalAuthenticationResourcePermissions.PolicyDefaultRoles, CoreVerbs.Update)) @@ -116,6 +119,9 @@ public sealed class ExternalAuthenticationRoleDeletionDependencyContributor( public async ValueTask RemoveEditableReferencesAsync(RoleReferenceRemovalRequest request, CancellationToken cancellationToken = default) { + var roleAuthorizationService = roleAuthorizationServices.SingleOrDefault(); + if (roleAuthorizationService is null) + return new RoleReferenceRemovalResult.Failed("role_authorization_unavailable", []); var validation = await ValidateRemovalAsync(request, cancellationToken); if (validation is RoleReferenceRemovalValidationResult.Forbidden forbidden) return new RoleReferenceRemovalResult.Failed(forbidden.Code, []); @@ -141,6 +147,9 @@ public sealed class ExternalAuthenticationRoleDeletionDependencyContributor( if (request.SelectedReferences is not null && removesLastDefaultRole) { + var roleStore = roleStores.SingleOrDefault(); + if (roleStore is null) + return new RoleReferenceRemovalResult.Failed("replacement_role_unavailable_or_unauthorized", changedOwnerIds); var replacement = await roleStore.FindAsync(new() { Id = request.ReplacementRoleId }, cancellationToken); if (replacement is null || !await roleAuthorizationService.CanAssignRolesAsync(request.Actor, [replacement.Id], cancellationToken)) diff --git a/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs b/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs index ae3fcd053..bd015e002 100644 --- a/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs +++ b/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs @@ -188,8 +188,8 @@ public class ExternalAuthenticationRoleDeletionDependencyContributorTests var contributor = new ExternalAuthenticationRoleDeletionDependencyContributor( connectionStore, new MutableOptionsMonitor(new ExternalAuthenticationOptions()), - roleAuthorizationService, - roleStore, + [roleAuthorizationService], + [roleStore], versions, new ConnectionRevisionCalculator(), new ExternalAuthenticationSecurityNotifier(services), @@ -291,8 +291,8 @@ public class ExternalAuthenticationRoleDeletionDependencyContributorTests var contributor = new ExternalAuthenticationRoleDeletionDependencyContributor( store, new MutableOptionsMonitor(new ExternalAuthenticationOptions { ConfigurationConnections = configuredConnections.ToList() }), - new RoleAuthorizationService(new StoreBasedRoleProvider(roleStore), new PermissionEvaluator()), - roleStore, + [new RoleAuthorizationService(new StoreBasedRoleProvider(roleStore), new PermissionEvaluator())], + [roleStore], versions, new ConnectionRevisionCalculator(), new ExternalAuthenticationSecurityNotifier(services), diff --git a/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationServiceCollectionTests.cs b/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationServiceCollectionTests.cs index c74ca8045..8e8bbae25 100644 --- a/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationServiceCollectionTests.cs +++ b/test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationServiceCollectionTests.cs @@ -9,6 +9,7 @@ using Elsa.ExternalAuthentication.Permissions; using Elsa.ExternalAuthentication.Providers; using Elsa.ExternalAuthentication.Services; using Elsa.ExternalAuthentication.Stores.InMemory; +using Elsa.Identity.Contracts; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -62,4 +63,23 @@ public class ExternalAuthenticationServiceCollectionTests Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(IProviderHttpClient) && descriptor.ImplementationFactory is not null); Assert.Contains(services, descriptor => descriptor.ServiceType == typeof(OpenIdConnectExternalAuthenticationAdapter)); } + + [Fact] + public void RoleDeletionContributorResolvesWhenIdentityIsNotRegistered() + { + var services = new ServiceCollection(); + services.AddSingleton(new TestSystemClock(DateTimeOffset.UnixEpoch)); + services.AddExternalAuthenticationServices(options => + { + options.AllowedUnlinkedIdentityPolicyTypes.Clear(); + options.AllowedPermissionGrantSourceTypes.Clear(); + }); + + using var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + using var scope = serviceProvider.CreateScope(); + + var contributor = Assert.Single(scope.ServiceProvider.GetServices()); + Assert.IsType(contributor); + Assert.Empty(scope.ServiceProvider.GetServices()); + } }