Skip to content

Commit 4dbe049

Browse files
authored
Merge pull request #25945 from abpframework/maliming/delete-user-password-histories-passkeys
Remove all related data when deleting a user
2 parents f68b773 + 3da670f commit 4dbe049

15 files changed

Lines changed: 862 additions & 12 deletions

File tree

framework/src/Volo.Abp.Gdpr.Abstractions/Volo.Abp.Gdpr.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<ItemGroup>
1414
<ProjectReference Include="..\Volo.Abp.Core\Volo.Abp.Core.csproj" />
15+
<ProjectReference Include="..\Volo.Abp.EventBus.Abstractions\Volo.Abp.EventBus.Abstractions.csproj" />
1516
</ItemGroup>
1617

1718
</Project>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
using Volo.Abp.Modularity;
2+
using Volo.Abp.EventBus.Abstractions;
23

34
namespace Volo.Abp.Gdpr;
45

6+
[DependsOn(
7+
typeof(AbpEventBusAbstractionsModule)
8+
)]
59
public class AbpGdprAbstractionsModule : AbpModule
610
{
711
}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
using System;
2+
using Volo.Abp.EventBus;
23

34
namespace Volo.Abp.Gdpr;
45

56
[Serializable]
6-
public class GdprUserDataDeletionRequestedEto
7+
public class GdprUserDataDeletionRequestedEto : IEventDataMayHaveTenantId
78
{
9+
public Guid? TenantId { get; set; }
10+
811
public Guid UserId { get; set; }
9-
}
12+
13+
public bool IsMultiTenant(out Guid? tenantId)
14+
{
15+
tenantId = TenantId;
16+
return TenantId.HasValue;
17+
}
18+
}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
using System;
2+
using Volo.Abp.EventBus;
23

34
namespace Volo.Abp.Gdpr;
45

56
[Serializable]
6-
public class GdprUserDataPreparedEto
7+
public class GdprUserDataPreparedEto : IEventDataMayHaveTenantId
78
{
9+
public Guid? TenantId { get; set; }
10+
811
public Guid RequestId { get; set; }
912

1013
public string Provider { get; set; } = default!;
1114

1215
public GdprDataInfo Data { get; set; } = default!;
13-
}
16+
17+
public bool IsMultiTenant(out Guid? tenantId)
18+
{
19+
tenantId = TenantId;
20+
return TenantId.HasValue;
21+
}
22+
}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
using System;
2+
using Volo.Abp.EventBus;
23

34
namespace Volo.Abp.Gdpr;
45

56
[Serializable]
6-
public class GdprUserDataRequestedEto
7+
public class GdprUserDataRequestedEto : IEventDataMayHaveTenantId
78
{
9+
public Guid? TenantId { get; set; }
10+
811
public Guid UserId { get; set; }
912

1013
public Guid RequestId { get; set; }
11-
}
14+
15+
public bool IsMultiTenant(out Guid? tenantId)
16+
{
17+
tenantId = TenantId;
18+
return TenantId.HasValue;
19+
}
20+
}

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,32 @@ public virtual async Task<IdentityResult> CreateAsync(IdentityUser user, string
9898

9999
public async override Task<IdentityResult> DeleteAsync(IdentityUser user)
100100
{
101+
//The user may have been loaded without details.
102+
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Claims, CancellationToken);
103+
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Roles, CancellationToken);
104+
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Tokens, CancellationToken);
105+
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Logins, CancellationToken);
106+
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.OrganizationUnits, CancellationToken);
107+
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.PasswordHistories, CancellationToken);
108+
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Passkeys, CancellationToken);
109+
101110
user.Claims.Clear();
102111
user.Roles.Clear();
103112
user.Tokens.Clear();
104113
user.Logins.Clear();
105114
user.OrganizationUnits.Clear();
106-
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId), CancellationToken);
107-
await UpdateAsync(user);
115+
user.PasswordHistories.Clear();
116+
user.Passkeys.Clear();
117+
118+
//Soft deleting reloads the original values, the store saves the changes without validating the user.
119+
//Nothing else is deleted before this succeeds, it is where the user is checked for concurrency.
120+
(await Store.UpdateAsync(user, CancellationToken)).CheckErrors();
121+
122+
//They are in the host database and deleting them here covers the current unit of work.
123+
using (CurrentTenant.Change(null))
124+
{
125+
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId), CancellationToken);
126+
}
108127

109128
return await base.DeleteAsync(user);
110129
}

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

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,51 @@ public virtual async Task CreateAsync(OrganizationUnit organizationUnit)
4848
await OrganizationUnitRepository.InsertAsync(organizationUnit);
4949
}
5050

51+
/// <summary>
52+
/// Creates the given organization units by querying the siblings of a parent once instead of once
53+
/// per organization unit. They all have to belong to the current tenant and their parents must
54+
/// already exist. <see cref="GetNextChildCodeAsync"/> is used for the first organization unit of a
55+
/// parent, the codes of the rest follow it. Custom validation should be added by overriding
56+
/// <see cref="ValidateOrganizationUnitAsync(OrganizationUnit, List{OrganizationUnit})"/>.
57+
/// </summary>
58+
[UnitOfWork]
59+
public virtual async Task CreateManyAsync(List<OrganizationUnit> organizationUnits)
60+
{
61+
Check.NotNull(organizationUnits, nameof(organizationUnits));
62+
63+
if (organizationUnits.Any(x => x.TenantId != CurrentTenant.Id))
64+
{
65+
throw new AbpException("Organization units of another tenant can not be created, change the current tenant instead!");
66+
}
67+
68+
var groups = organizationUnits.GroupBy(x => x.ParentId).ToList();
69+
70+
foreach (var group in groups)
71+
{
72+
await ValidateParentTenantAsync(group.Key, CurrentTenant.Id);
73+
74+
var siblings = await FindChildrenAsync(group.Key);
75+
string lastCode = null;
76+
77+
foreach (var organizationUnit in group)
78+
{
79+
organizationUnit.Code = lastCode = lastCode == null
80+
? await GetNextChildCodeAsync(group.Key)
81+
: OrganizationUnit.CalculateNextCode(lastCode);
82+
83+
await ValidateOrganizationUnitAsync(organizationUnit, siblings);
84+
85+
siblings.Add(organizationUnit);
86+
}
87+
}
88+
89+
//Nothing is inserted before every group is validated.
90+
foreach (var group in groups)
91+
{
92+
await OrganizationUnitRepository.InsertManyAsync(group.ToList());
93+
}
94+
}
95+
5196
public virtual async Task UpdateAsync(OrganizationUnit organizationUnit)
5297
{
5398
await ValidateOrganizationUnitAsync(organizationUnit);
@@ -141,15 +186,21 @@ public virtual async Task<string> GetCodeOrDefaultAsync(Guid id)
141186

142187
protected virtual async Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit)
143188
{
144-
var siblings = (await FindChildrenAsync(organizationUnit.ParentId))
145-
.Where(ou => ou.Id != organizationUnit.Id)
146-
.ToList();
189+
await ValidateOrganizationUnitAsync(organizationUnit, await FindChildrenAsync(organizationUnit.ParentId));
190+
}
147191

148-
if (siblings.Any(ou => ou.DisplayName == organizationUnit.DisplayName))
192+
/// <summary>
193+
/// Validates the organization unit against the given siblings, so they are not queried again.
194+
/// </summary>
195+
protected virtual Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit, List<OrganizationUnit> siblings)
196+
{
197+
if (siblings.Any(ou => ou.Id != organizationUnit.Id && ou.DisplayName == organizationUnit.DisplayName))
149198
{
150199
throw new BusinessException(IdentityErrorCodes.DuplicateOrganizationUnitDisplayName)
151200
.WithData("0", organizationUnit.DisplayName);
152201
}
202+
203+
return Task.CompletedTask;
153204
}
154205

155206
protected virtual async Task ValidateParentTenantAsync(Guid? parentId, Guid? tenantId)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using Volo.Abp.DependencyInjection;
4+
using Volo.Abp.Domain.Entities.Events;
5+
using Volo.Abp.EventBus;
6+
using Volo.Abp.MultiTenancy;
7+
using Volo.Abp.Uow;
8+
9+
namespace Volo.Abp.Identity;
10+
11+
//Sessions, user delegations and link users have no navigation from IdentityUser,
12+
//so clearing the user's collections doesn't cover them.
13+
public class UserDeletedEventHandler :
14+
ILocalEventHandler<EntityDeletedEventData<IdentityUser>>,
15+
ITransientDependency
16+
{
17+
protected IIdentitySessionRepository IdentitySessionRepository { get; }
18+
protected IIdentityUserDelegationRepository IdentityUserDelegationRepository { get; }
19+
protected IIdentityLinkUserRepository IdentityLinkUserRepository { get; }
20+
protected ICurrentTenant CurrentTenant { get; }
21+
22+
public UserDeletedEventHandler(
23+
IIdentitySessionRepository identitySessionRepository,
24+
IIdentityUserDelegationRepository identityUserDelegationRepository,
25+
IIdentityLinkUserRepository identityLinkUserRepository,
26+
ICurrentTenant currentTenant)
27+
{
28+
IdentitySessionRepository = identitySessionRepository;
29+
IdentityUserDelegationRepository = identityUserDelegationRepository;
30+
IdentityLinkUserRepository = identityLinkUserRepository;
31+
CurrentTenant = currentTenant;
32+
}
33+
34+
[UnitOfWork]
35+
public virtual async Task HandleEventAsync(EntityDeletedEventData<IdentityUser> eventData)
36+
{
37+
var user = eventData.Entity;
38+
39+
await IdentitySessionRepository.DeleteAllAsync(user.Id);
40+
41+
var delegations = await IdentityUserDelegationRepository.GetListAsync(sourceUserId: user.Id, targetUserId: null);
42+
delegations.AddRange(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: null, targetUserId: user.Id));
43+
//A delegation of the user to itself is returned by both queries.
44+
await IdentityUserDelegationRepository.DeleteManyAsync(delegations.DistinctBy(x => x.Id).ToList());
45+
46+
//Link users are stored in the host database.
47+
using (CurrentTenant.Change(null))
48+
{
49+
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId));
50+
}
51+
}
52+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace Volo.Abp.Identity;
2+
3+
public class IdentityUserManager_Delete_Tests : IdentityUserManager_Delete_Tests<AbpIdentityDomainTestModule>
4+
{
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace Volo.Abp.Identity;
2+
3+
public class OrganizationUnitManager_CreateMany_Tests : OrganizationUnitManager_CreateMany_Tests<AbpIdentityDomainTestModule>
4+
{
5+
}

0 commit comments

Comments
 (0)