Browse Source

Remove all related data when deleting a user

- Clear password histories and passkeys, load the collections if they are missing
- Delete sessions, user delegations and link users with UserDeletedEventHandler
- Add OrganizationUnitManager.CreateManyAsync
pull/25945/head
maliming 4 days ago
parent
commit
4196f1c816
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 23
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs
  2. 50
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs
  3. 52
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserDeletedEventHandler.cs
  4. 5
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs
  5. 53
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_Tests.cs
  6. 8
      modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityUserManager_Delete_Tests.cs
  7. 217
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs

23
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs

@ -98,13 +98,32 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
public async override Task<IdentityResult> DeleteAsync(IdentityUser user)
{
//The user may have been loaded without details.
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Claims, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Roles, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Tokens, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Logins, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.OrganizationUnits, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.PasswordHistories, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Passkeys, CancellationToken);
user.Claims.Clear();
user.Roles.Clear();
user.Tokens.Clear();
user.Logins.Clear();
user.OrganizationUnits.Clear();
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId), CancellationToken);
await UpdateAsync(user);
user.PasswordHistories.Clear();
user.Passkeys.Clear();
//UserDeletedEventHandler deletes them after the changes are saved, this keeps
//them gone for the rest of the current unit of work. They are in the host database.
using (CurrentTenant.Change(null))
{
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId), CancellationToken);
}
//Soft deleting an entity reloads its original values.
(await UpdateAsync(user)).CheckErrors();
return await base.DeleteAsync(user);
}

50
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs

@ -48,6 +48,42 @@ public class OrganizationUnitManager : DomainService
await OrganizationUnitRepository.InsertAsync(organizationUnit);
}
/// <summary>
/// Creates the given organization units by querying the siblings of a parent once instead of once
/// per organization unit. The parents must already exist. Custom validation should be added by
/// overriding <see cref="ValidateOrganizationUnitAsync(OrganizationUnit, List{OrganizationUnit})"/>.
/// </summary>
[UnitOfWork]
public virtual async Task CreateManyAsync(List<OrganizationUnit> organizationUnits)
{
Check.NotNull(organizationUnits, nameof(organizationUnits));
foreach (var group in organizationUnits.GroupBy(x => new { x.TenantId, x.ParentId }))
{
//Siblings, codes and the database of a group belong to its own tenant.
using (CurrentTenant.Change(group.Key.TenantId))
{
await ValidateParentTenantAsync(group.Key.ParentId, group.Key.TenantId);
var siblings = await FindChildrenAsync(group.Key.ParentId);
var lastCode = siblings.OrderBy(x => x.Code).LastOrDefault()?.Code;
foreach (var organizationUnit in group)
{
await ValidateOrganizationUnitAsync(organizationUnit, siblings);
organizationUnit.Code = lastCode = lastCode == null
? await GetNextChildCodeAsync(group.Key.ParentId)
: OrganizationUnit.CalculateNextCode(lastCode);
siblings.Add(organizationUnit);
}
await OrganizationUnitRepository.InsertManyAsync(group.ToList());
}
}
}
public virtual async Task UpdateAsync(OrganizationUnit organizationUnit)
{
await ValidateOrganizationUnitAsync(organizationUnit);
@ -141,15 +177,21 @@ public class OrganizationUnitManager : DomainService
protected virtual async Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit)
{
var siblings = (await FindChildrenAsync(organizationUnit.ParentId))
.Where(ou => ou.Id != organizationUnit.Id)
.ToList();
await ValidateOrganizationUnitAsync(organizationUnit, await FindChildrenAsync(organizationUnit.ParentId));
}
if (siblings.Any(ou => ou.DisplayName == organizationUnit.DisplayName))
/// <summary>
/// Validates the organization unit against the given siblings, so they are not queried again.
/// </summary>
protected virtual Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit, List<OrganizationUnit> siblings)
{
if (siblings.Any(ou => ou.Id != organizationUnit.Id && ou.DisplayName == organizationUnit.DisplayName))
{
throw new BusinessException(IdentityErrorCodes.DuplicateOrganizationUnitDisplayName)
.WithData("0", organizationUnit.DisplayName);
}
return Task.CompletedTask;
}
protected virtual async Task ValidateParentTenantAsync(Guid? parentId, Guid? tenantId)

52
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserDeletedEventHandler.cs

@ -0,0 +1,52 @@
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace Volo.Abp.Identity;
//Sessions, user delegations and link users have no navigation from IdentityUser,
//so clearing the user's collections doesn't cover them.
public class UserDeletedEventHandler :
ILocalEventHandler<EntityDeletedEventData<IdentityUser>>,
ITransientDependency
{
protected IIdentitySessionRepository IdentitySessionRepository { get; }
protected IIdentityUserDelegationRepository IdentityUserDelegationRepository { get; }
protected IIdentityLinkUserRepository IdentityLinkUserRepository { get; }
protected ICurrentTenant CurrentTenant { get; }
public UserDeletedEventHandler(
IIdentitySessionRepository identitySessionRepository,
IIdentityUserDelegationRepository identityUserDelegationRepository,
IIdentityLinkUserRepository identityLinkUserRepository,
ICurrentTenant currentTenant)
{
IdentitySessionRepository = identitySessionRepository;
IdentityUserDelegationRepository = identityUserDelegationRepository;
IdentityLinkUserRepository = identityLinkUserRepository;
CurrentTenant = currentTenant;
}
[UnitOfWork]
public virtual async Task HandleEventAsync(EntityDeletedEventData<IdentityUser> eventData)
{
var user = eventData.Entity;
await IdentitySessionRepository.DeleteAllAsync(user.Id);
var delegations = await IdentityUserDelegationRepository.GetListAsync(sourceUserId: user.Id, targetUserId: null);
delegations.AddRange(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: null, targetUserId: user.Id));
//A delegation of the user to itself is returned by both queries.
await IdentityUserDelegationRepository.DeleteManyAsync(delegations.DistinctBy(x => x.Id).ToList());
//Link users are stored in the host database.
using (CurrentTenant.Change(null))
{
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId));
}
}
}

5
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs

@ -0,0 +1,5 @@
namespace Volo.Abp.Identity;
public class IdentityUserManager_Delete_Tests : IdentityUserManager_Delete_Tests<AbpIdentityDomainTestModule>
{
}

53
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_Tests.cs

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Identity;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Data;
@ -179,4 +180,56 @@ public class OrganizationUnitManager_Tests : AbpIdentityDomainTestBase
}
}
}
[Fact]
public async Task CreateManyAsync()
{
List<OrganizationUnit> organizationUnits;
using (var uow = _unitOfWorkManager.Begin())
{
var lastRootCode = (await _organizationUnitRepository.GetChildrenAsync(null))
.OrderBy(x => x.Code).Last().Code;
organizationUnits = Enumerable.Range(0, 5)
.Select(_ => new OrganizationUnit(_guidGenerator.Create(), $"batch-{Guid.NewGuid():N}"))
.ToList();
await _organizationUnitManager.CreateManyAsync(organizationUnits);
await uow.CompleteAsync();
foreach (var organizationUnit in organizationUnits)
{
lastRootCode = OrganizationUnit.CalculateNextCode(lastRootCode);
organizationUnit.Code.ShouldBe(lastRootCode);
}
}
using (var uow = _unitOfWorkManager.Begin())
{
foreach (var organizationUnit in organizationUnits)
{
(await _organizationUnitRepository.GetAsync(organizationUnit.Id)).Code.ShouldBe(organizationUnit.Code);
}
await uow.CompleteAsync();
}
}
[Fact]
public async Task CreateManyAsync_Should_Not_Allow_Duplicate_Display_Name_In_The_Batch()
{
using (var uow = _unitOfWorkManager.Begin())
{
var displayName = $"batch-duplicate-{Guid.NewGuid():N}";
await Should.ThrowAsync<BusinessException>(async () =>
await _organizationUnitManager.CreateManyAsync([
new OrganizationUnit(_guidGenerator.Create(), displayName),
new OrganizationUnit(_guidGenerator.Create(), displayName)
]));
await uow.CompleteAsync();
}
}
}

8
modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityUserManager_Delete_Tests.cs

@ -0,0 +1,8 @@
using Xunit;
namespace Volo.Abp.Identity.MongoDB;
[Collection(MongoTestCollection.Name)]
public class IdentityUserManager_Delete_Tests : IdentityUserManager_Delete_Tests<AbpIdentityMongoDbTestModule>
{
}

217
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs

@ -0,0 +1,217 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.Modularity;
using Volo.Abp.Uow;
using Xunit;
namespace Volo.Abp.Identity;
public abstract class IdentityUserManager_Delete_Tests<TStartupModule> : AbpIdentityTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
protected IdentityUserManager IdentityUserManager { get; }
protected IIdentityUserRepository IdentityUserRepository { get; }
protected IOrganizationUnitRepository OrganizationUnitRepository { get; }
protected IdentityLinkUserManager IdentityLinkUserManager { get; }
protected IIdentitySessionRepository IdentitySessionRepository { get; }
protected IIdentityUserDelegationRepository IdentityUserDelegationRepository { get; }
protected ILookupNormalizer LookupNormalizer { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IDataFilter DataFilter { get; }
protected IdentityUserManager_Delete_Tests()
{
IdentityUserManager = GetRequiredService<IdentityUserManager>();
IdentityUserRepository = GetRequiredService<IIdentityUserRepository>();
OrganizationUnitRepository = GetRequiredService<IOrganizationUnitRepository>();
IdentityLinkUserManager = GetRequiredService<IdentityLinkUserManager>();
IdentitySessionRepository = GetRequiredService<IIdentitySessionRepository>();
IdentityUserDelegationRepository = GetRequiredService<IIdentityUserDelegationRepository>();
LookupNormalizer = GetRequiredService<ILookupNormalizer>();
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
DataFilter = GetRequiredService<IDataFilter>();
}
[Fact]
public virtual async Task DeleteAsync_Should_Remove_All_Related_Data()
{
var userId = Guid.NewGuid();
var linkedUserId = Guid.NewGuid();
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"delete-{userId:N}", $"delete-{userId:N}@abp.io"))).CheckErrors();
(await IdentityUserManager.CreateAsync(
new IdentityUser(linkedUserId, $"linked-{linkedUserId:N}", $"linked-{linkedUserId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserManager.GetByIdAsync(userId);
await IdentityUserManager.AddClaimAsync(user, new Claim("test", "test"));
await IdentityUserManager.AddLoginAsync(user, new UserLoginInfo("test", "test", "test"));
await IdentityUserManager.AddToRoleAsync(user, "moderator");
user.SetToken("test", "test", "test");
user.AddPasswordHistory("test");
user.AddPasskey([1, 2, 3], new IdentityPasskeyData());
await IdentityUserManager.AddToOrganizationUnitAsync(
user,
await OrganizationUnitRepository.GetAsync(LookupNormalizer.NormalizeName("OU11")));
await IdentityLinkUserManager.LinkAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId));
await IdentitySessionRepository.InsertAsync(new IdentitySession(
Guid.NewGuid(), $"session-{userId:N}", "Web", "Chrome", userId, null, "MyApp", "127.0.0.1", DateTime.UtcNow));
await IdentityUserDelegationRepository.InsertAsync(new IdentityUserDelegation(
Guid.NewGuid(), userId, linkedUserId, DateTime.UtcNow, DateTime.UtcNow.AddDays(1)));
await IdentityUserDelegationRepository.InsertAsync(new IdentityUserDelegation(
Guid.NewGuid(), linkedUserId, userId, DateTime.UtcNow, DateTime.UtcNow.AddDays(1)));
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserManager.GetByIdAsync(userId);
user.Claims.Count.ShouldBeGreaterThan(0);
user.Logins.Count.ShouldBeGreaterThan(0);
user.Roles.Count.ShouldBeGreaterThan(0);
user.Tokens.Count.ShouldBeGreaterThan(0);
user.OrganizationUnits.Count.ShouldBeGreaterThan(0);
user.PasswordHistories.Count.ShouldBeGreaterThan(0);
user.Passkeys.Count.ShouldBeGreaterThan(0);
(await IdentityUserManager.DeleteAsync(user)).CheckErrors();
await uow.CompleteAsync();
}
//The user is soft deleted, disable the filter to see what is left behind.
using (var uow = UnitOfWorkManager.Begin())
using (DataFilter.Disable<ISoftDelete>())
{
var deletedUser = await IdentityUserRepository.FindAsync(userId);
deletedUser.ShouldNotBeNull();
deletedUser.Claims.Count.ShouldBe(0);
deletedUser.Logins.Count.ShouldBe(0);
deletedUser.Roles.Count.ShouldBe(0);
deletedUser.Tokens.Count.ShouldBe(0);
deletedUser.OrganizationUnits.Count.ShouldBe(0);
deletedUser.PasswordHistories.Count.ShouldBe(0);
deletedUser.Passkeys.Count.ShouldBe(0);
(await IdentityLinkUserManager.IsLinkedAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId))).ShouldBeFalse();
(await IdentitySessionRepository.GetCountAsync(userId: userId)).ShouldBe(0);
(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: userId, targetUserId: null)).ShouldBeEmpty();
(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: null, targetUserId: userId)).ShouldBeEmpty();
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task DeleteAsync_Should_Remove_Related_Data_Of_A_User_Loaded_Without_Details()
{
var userId = Guid.NewGuid();
var userName = $"no-details-{userId:N}";
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, userName, $"{userName}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserManager.GetByIdAsync(userId);
await IdentityUserManager.AddToRoleAsync(user, "moderator");
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserRepository.FindByNormalizedUserNameAsync(
LookupNormalizer.NormalizeName(userName),
includeDetails: false);
(await IdentityUserManager.DeleteAsync(user)).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
using (DataFilter.Disable<ISoftDelete>())
{
var deletedUser = await IdentityUserRepository.FindAsync(userId);
deletedUser.ShouldNotBeNull();
deletedUser.Roles.Count.ShouldBe(0);
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task Deleting_A_User_Through_The_Repository_Should_Remove_Sessions_Delegations_And_Links()
{
var userId = Guid.NewGuid();
var linkedUserId = Guid.NewGuid();
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"repo-{userId:N}", $"repo-{userId:N}@abp.io"))).CheckErrors();
(await IdentityUserManager.CreateAsync(
new IdentityUser(linkedUserId, $"repo-linked-{linkedUserId:N}", $"repo-linked-{linkedUserId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
await IdentityLinkUserManager.LinkAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId));
await IdentitySessionRepository.InsertAsync(new IdentitySession(
Guid.NewGuid(), $"repo-session-{userId:N}", "Web", "Chrome", userId, null, "MyApp", "127.0.0.1", DateTime.UtcNow));
await IdentityUserDelegationRepository.InsertAsync(new IdentityUserDelegation(
Guid.NewGuid(), userId, linkedUserId, DateTime.UtcNow, DateTime.UtcNow.AddDays(1)));
await uow.CompleteAsync();
}
//Custom code may delete the user without using IdentityUserManager.
using (var uow = UnitOfWorkManager.Begin())
{
await IdentityUserRepository.DeleteAsync(await IdentityUserRepository.GetAsync(userId));
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentitySessionRepository.GetCountAsync(userId: userId)).ShouldBe(0);
(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: userId, targetUserId: null)).ShouldBeEmpty();
(await IdentityLinkUserManager.IsLinkedAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId))).ShouldBeFalse();
await uow.CompleteAsync();
}
}
}
Loading…
Cancel
Save