Guard optional Identity role services

This commit is contained in:
Sipke Schoorstra 2026-09-06 03:54:35 +02:00
parent c1ea463e0e
commit 62fcc302c7
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
3 changed files with 35 additions and 6 deletions

View file

@ -20,8 +20,8 @@ namespace Elsa.ExternalAuthentication.Services;
public sealed class ExternalAuthenticationRoleDeletionDependencyContributor(
IIdentityProviderConnectionStore store,
IOptionsMonitor<ExternalAuthenticationOptions> options,
IRoleAuthorizationService roleAuthorizationService,
IRoleStore roleStore,
IEnumerable<IRoleAuthorizationService> roleAuthorizationServices,
IEnumerable<IRoleStore> roleStores,
IConnectionRegistryVersionStore registryVersions,
ConnectionRevisionCalculator revisionCalculator,
ExternalAuthenticationSecurityNotifier notifier,
@ -67,6 +67,9 @@ public sealed class ExternalAuthenticationRoleDeletionDependencyContributor(
public async ValueTask<RoleReferenceRemovalValidationResult> 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<RoleReferenceRemovalResult> 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))

View file

@ -188,8 +188,8 @@ public class ExternalAuthenticationRoleDeletionDependencyContributorTests
var contributor = new ExternalAuthenticationRoleDeletionDependencyContributor(
connectionStore,
new MutableOptionsMonitor<ExternalAuthenticationOptions>(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<ExternalAuthenticationOptions>(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),

View file

@ -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<ISystemClock>(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<IRoleDeletionDependencyContributor>());
Assert.IsType<ExternalAuthenticationRoleDeletionDependencyContributor>(contributor);
Assert.Empty(scope.ServiceProvider.GetServices<IRoleStore>());
}
}