Skip to content

Commit 3da670f

Browse files
committed
Carry the tenant id in the GDPR events
- Delete the link users after the user is saved, a concurrency failure kept them deleted - Create organization units of the current tenant only and insert them after every group is valid
1 parent dd396d1 commit 3da670f

12 files changed

Lines changed: 339 additions & 28 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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,16 @@ public async override Task<IdentityResult> DeleteAsync(IdentityUser user)
115115
user.PasswordHistories.Clear();
116116
user.Passkeys.Clear();
117117

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+
118122
//They are in the host database and deleting them here covers the current unit of work.
119123
using (CurrentTenant.Change(null))
120124
{
121125
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId), CancellationToken);
122126
}
123127

124-
//Soft deleting reloads the original values, the store saves the changes without validating the user.
125-
(await Store.UpdateAsync(user, CancellationToken)).CheckErrors();
126-
127128
return await base.DeleteAsync(user);
128129
}
129130

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

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,38 +50,47 @@ public virtual async Task CreateAsync(OrganizationUnit organizationUnit)
5050

5151
/// <summary>
5252
/// Creates the given organization units by querying the siblings of a parent once instead of once
53-
/// per organization unit. The parents must already exist. Custom validation should be added by
54-
/// overriding <see cref="ValidateOrganizationUnitAsync(OrganizationUnit, List{OrganizationUnit})"/>.
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})"/>.
5557
/// </summary>
5658
[UnitOfWork]
5759
public virtual async Task CreateManyAsync(List<OrganizationUnit> organizationUnits)
5860
{
5961
Check.NotNull(organizationUnits, nameof(organizationUnits));
6062

61-
foreach (var group in organizationUnits.GroupBy(x => new { x.TenantId, x.ParentId }))
63+
if (organizationUnits.Any(x => x.TenantId != CurrentTenant.Id))
6264
{
63-
//Siblings, codes and the database of a group belong to its own tenant.
64-
using (CurrentTenant.Change(group.Key.TenantId))
65-
{
66-
await ValidateParentTenantAsync(group.Key.ParentId, group.Key.TenantId);
65+
throw new AbpException("Organization units of another tenant can not be created, change the current tenant instead!");
66+
}
6767

68-
var siblings = await FindChildrenAsync(group.Key.ParentId);
69-
var lastCode = siblings.OrderBy(x => x.Code).LastOrDefault()?.Code;
68+
var groups = organizationUnits.GroupBy(x => x.ParentId).ToList();
69+
70+
foreach (var group in groups)
71+
{
72+
await ValidateParentTenantAsync(group.Key, CurrentTenant.Id);
7073

71-
foreach (var organizationUnit in group)
72-
{
73-
await ValidateOrganizationUnitAsync(organizationUnit, siblings);
74+
var siblings = await FindChildrenAsync(group.Key);
75+
string lastCode = null;
7476

75-
organizationUnit.Code = lastCode = lastCode == null
76-
? await GetNextChildCodeAsync(group.Key.ParentId)
77-
: OrganizationUnit.CalculateNextCode(lastCode);
77+
foreach (var organizationUnit in group)
78+
{
79+
organizationUnit.Code = lastCode = lastCode == null
80+
? await GetNextChildCodeAsync(group.Key)
81+
: OrganizationUnit.CalculateNextCode(lastCode);
7882

79-
siblings.Add(organizationUnit);
80-
}
83+
await ValidateOrganizationUnitAsync(organizationUnit, siblings);
8184

82-
await OrganizationUnitRepository.InsertManyAsync(group.ToList());
85+
siblings.Add(organizationUnit);
8386
}
8487
}
88+
89+
//Nothing is inserted before every group is validated.
90+
foreach (var group in groups)
91+
{
92+
await OrganizationUnitRepository.InsertManyAsync(group.ToList());
93+
}
8594
}
8695

8796
public virtual async Task UpdateAsync(OrganizationUnit organizationUnit)
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+
}

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
using System.Linq;
66
using System.Threading.Tasks;
77
using Volo.Abp.Data;
8+
using Volo.Abp.DependencyInjection;
9+
using Volo.Abp.Threading;
10+
using Volo.Abp.Security.Claims;
11+
using Volo.Abp.Identity.Localization;
12+
using Volo.Abp.Caching;
13+
using Microsoft.Extensions.Localization;
814
using Volo.Abp.Guids;
915
using Volo.Abp.MultiTenancy;
1016
using Volo.Abp.Uow;
@@ -232,4 +238,84 @@ await _organizationUnitManager.CreateManyAsync([
232238
await uow.CompleteAsync();
233239
}
234240
}
241+
242+
[Fact]
243+
public async Task CreateManyAsync_Should_Not_Create_Organization_Units_Of_Another_Tenant()
244+
{
245+
using (var uow = _unitOfWorkManager.Begin())
246+
{
247+
await Should.ThrowAsync<AbpException>(async () =>
248+
await _organizationUnitManager.CreateManyAsync([
249+
new OrganizationUnit(_guidGenerator.Create(), "another-tenant", null, Guid.NewGuid())
250+
]));
251+
252+
await uow.CompleteAsync();
253+
}
254+
}
255+
256+
[Fact]
257+
public async Task CreateManyAsync_Should_Use_The_Overridden_Extension_Points()
258+
{
259+
var manager = new TestOrganizationUnitManager(
260+
_organizationUnitRepository,
261+
GetRequiredService<IStringLocalizer<IdentityResource>>(),
262+
_identityRoleRepository,
263+
GetRequiredService<IDistributedCache<AbpDynamicClaimCacheItem>>(),
264+
GetRequiredService<ICancellationTokenProvider>())
265+
{
266+
LazyServiceProvider = GetRequiredService<IAbpLazyServiceProvider>()
267+
};
268+
269+
using (var uow = _unitOfWorkManager.Begin())
270+
{
271+
await Should.ThrowAsync<BusinessException>(async () =>
272+
await manager.CreateManyAsync([new OrganizationUnit(_guidGenerator.Create(), "rejected-by-the-override")]));
273+
274+
await manager.CreateManyAsync([
275+
new OrganizationUnit(_guidGenerator.Create(), $"extension-point-1-{Guid.NewGuid():N}"),
276+
new OrganizationUnit(_guidGenerator.Create(), $"extension-point-2-{Guid.NewGuid():N}")
277+
]);
278+
279+
await uow.CompleteAsync();
280+
}
281+
282+
//Every organization unit is validated, the code generator is only used for the first one of a parent.
283+
//Both calls above created a root organization unit, so the code generator was used twice.
284+
manager.ValidateCallCount.ShouldBe(3);
285+
manager.GetNextChildCodeCallCount.ShouldBe(2);
286+
}
287+
288+
public class TestOrganizationUnitManager : OrganizationUnitManager
289+
{
290+
public int ValidateCallCount { get; private set; }
291+
public int GetNextChildCodeCallCount { get; private set; }
292+
293+
public TestOrganizationUnitManager(
294+
IOrganizationUnitRepository organizationUnitRepository,
295+
IStringLocalizer<IdentityResource> localizer,
296+
IIdentityRoleRepository identityRoleRepository,
297+
IDistributedCache<AbpDynamicClaimCacheItem> dynamicClaimCache,
298+
ICancellationTokenProvider cancellationTokenProvider)
299+
: base(organizationUnitRepository, localizer, identityRoleRepository, dynamicClaimCache, cancellationTokenProvider)
300+
{
301+
}
302+
303+
public override async Task<string> GetNextChildCodeAsync(Guid? parentId)
304+
{
305+
GetNextChildCodeCallCount++;
306+
return await base.GetNextChildCodeAsync(parentId);
307+
}
308+
309+
protected override async Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit, List<OrganizationUnit> siblings)
310+
{
311+
ValidateCallCount++;
312+
313+
if (organizationUnit.DisplayName == "rejected-by-the-override")
314+
{
315+
throw new BusinessException(IdentityErrorCodes.DuplicateOrganizationUnitDisplayName);
316+
}
317+
318+
await base.ValidateOrganizationUnitAsync(organizationUnit, siblings);
319+
}
320+
}
235321
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Xunit;
2+
3+
namespace Volo.Abp.Identity.MongoDB;
4+
5+
[Collection(MongoTestCollection.Name)]
6+
public class OrganizationUnitManager_CreateMany_Tests : OrganizationUnitManager_CreateMany_Tests<AbpIdentityMongoDbTestModule>
7+
{
8+
}

0 commit comments

Comments
 (0)