Skip to content

Commit 6d80fde

Browse files
author
Scott Bommarito
authored
AccountDelete.DeletedBy and PackageDelete.DeletedBy should be set to null when the User is deleted (#7104)
1 parent 33ef003 commit 6d80fde

9 files changed

Lines changed: 284 additions & 7 deletions

File tree

src/NuGet.Services.Entities/AccountDelete.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,19 @@ public class AccountDelete
2828
public User DeletedAccount { get; set; }
2929

3030
/// <summary>
31-
/// The User(admin) key that executed the delete action.
31+
/// The User key that executed the delete action.
3232
/// </summary>
33-
public int DeletedByKey { get; set; }
33+
/// <remarks>
34+
/// <c>null</c> if the user was deleted.
35+
/// </remarks>
36+
public int? DeletedByKey { get; set; }
3437

3538
/// <summary>
36-
/// The User(admin) that executed the delete action.
39+
/// The User that executed the delete action.
3740
/// </summary>
41+
/// <remarks>
42+
/// <c>null</c> if the user was deleted.
43+
/// </remarks>
3844
public User DeletedBy { get; set; }
3945

4046
/// <summary>

src/NuGet.Services.Entities/PackageDelete.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ public PackageDelete()
2020
[Required]
2121
public DateTime DeletedOn { get; set; }
2222

23-
[Required]
23+
/// <remarks>
24+
/// <c>null</c> if the user was deleted.
25+
/// </remarks>
2426
public User DeletedBy { get; set; }
25-
public int DeletedByKey { get; set; }
27+
28+
/// <remarks>
29+
/// <c>null</c> if the user was deleted.
30+
/// </remarks>
31+
public int? DeletedByKey { get; set; }
2632

2733
[Required]
2834
public string Reason { get; set; }

src/NuGetGallery.Core/Entities/EntitiesContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder)
356356
.HasRequired(a => a.DeletedAccount);
357357

358358
modelBuilder.Entity<AccountDelete>()
359-
.HasRequired(a => a.DeletedBy)
359+
.HasOptional(a => a.DeletedBy)
360360
.WithMany()
361361
.WillCascadeOnDelete(false);
362362

src/NuGetGallery/Migrations/201904252152567_DeletedByOptional.Designer.cs

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace NuGetGallery.Migrations
2+
{
3+
using System;
4+
using System.Data.Entity.Migrations;
5+
6+
public partial class DeletedByOptional : DbMigration
7+
{
8+
public override void Up()
9+
{
10+
DropForeignKey("dbo.PackageDeletes", "DeletedByKey", "dbo.Users");
11+
DropIndex("dbo.PackageDeletes", new[] { "DeletedByKey" });
12+
DropIndex("dbo.AccountDeletes", new[] { "DeletedByKey" });
13+
AlterColumn("dbo.PackageDeletes", "DeletedByKey", c => c.Int());
14+
AlterColumn("dbo.AccountDeletes", "DeletedByKey", c => c.Int());
15+
CreateIndex("dbo.PackageDeletes", "DeletedByKey");
16+
CreateIndex("dbo.AccountDeletes", "DeletedByKey");
17+
AddForeignKey("dbo.PackageDeletes", "DeletedByKey", "dbo.Users", "Key");
18+
}
19+
20+
public override void Down()
21+
{
22+
DropForeignKey("dbo.PackageDeletes", "DeletedByKey", "dbo.Users");
23+
DropIndex("dbo.AccountDeletes", new[] { "DeletedByKey" });
24+
DropIndex("dbo.PackageDeletes", new[] { "DeletedByKey" });
25+
AlterColumn("dbo.AccountDeletes", "DeletedByKey", c => c.Int(nullable: false));
26+
AlterColumn("dbo.PackageDeletes", "DeletedByKey", c => c.Int(nullable: false));
27+
CreateIndex("dbo.AccountDeletes", "DeletedByKey");
28+
CreateIndex("dbo.PackageDeletes", "DeletedByKey");
29+
AddForeignKey("dbo.PackageDeletes", "DeletedByKey", "dbo.Users", "Key", cascadeDelete: true);
30+
}
31+
}
32+
}

src/NuGetGallery/Migrations/201904252152567_DeletedByOptional.resx

Lines changed: 126 additions & 0 deletions
Large diffs are not rendered by default.

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@
238238
<Compile Include="Migrations\201904081230004_AddIndexToUserSecurityPolicy.Designer.cs">
239239
<DependentUpon>201904081230004_AddIndexToUserSecurityPolicy.cs</DependentUpon>
240240
</Compile>
241+
<Compile Include="Migrations\201904252152567_DeletedByOptional.cs" />
242+
<Compile Include="Migrations\201904252152567_DeletedByOptional.Designer.cs">
243+
<DependentUpon>201904252152567_DeletedByOptional.cs</DependentUpon>
244+
</Compile>
241245
<Compile Include="Queries\AutocompleteCveIdQueryResults.cs" />
242246
<Compile Include="Queries\AutocompleteCweIdQueryResults.cs" />
243247
<Compile Include="Queries\AutocompleteCveIdQueryResult.cs" />
@@ -1722,6 +1726,9 @@
17221726
<EmbeddedResource Include="Migrations\201904081230004_AddIndexToUserSecurityPolicy.resx">
17231727
<DependentUpon>201904081230004_AddIndexToUserSecurityPolicy.cs</DependentUpon>
17241728
</EmbeddedResource>
1729+
<EmbeddedResource Include="Migrations\201904252152567_DeletedByOptional.resx">
1730+
<DependentUpon>201904252152567_DeletedByOptional.cs</DependentUpon>
1731+
</EmbeddedResource>
17251732
<EmbeddedResource Include="OData\QueryAllowed\Data\apiv1packages.json" />
17261733
<EmbeddedResource Include="OData\QueryAllowed\Data\apiv1search.json" />
17271734
<EmbeddedResource Include="OData\QueryAllowed\Data\apiv2getupdates.json" />

src/NuGetGallery/Services/DeleteAccountService.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace NuGetGallery
1919
public class DeleteAccountService : IDeleteAccountService
2020
{
2121
private readonly IEntityRepository<AccountDelete> _accountDeleteRepository;
22+
private readonly IEntityRepository<PackageDelete> _packageDeleteRepository;
2223
private readonly IEntitiesContext _entitiesContext;
2324
private readonly IPackageService _packageService;
2425
private readonly IPackageOwnershipManagementService _packageOwnershipManagementService;
@@ -35,6 +36,7 @@ public class DeleteAccountService : IDeleteAccountService
3536

3637
public DeleteAccountService(
3738
IEntityRepository<AccountDelete> accountDeleteRepository,
39+
IEntityRepository<PackageDelete> packageDeleteRepository,
3840
IEntityRepository<PackageDeprecation> deprecationRepository,
3941
IEntityRepository<User> userRepository,
4042
IEntityRepository<Scope> scopeRepository,
@@ -50,6 +52,7 @@ public DeleteAccountService(
5052
ITelemetryService telemetryService)
5153
{
5254
_accountDeleteRepository = accountDeleteRepository ?? throw new ArgumentNullException(nameof(accountDeleteRepository));
55+
_packageDeleteRepository = packageDeleteRepository ?? throw new ArgumentNullException(nameof(packageDeleteRepository));
5356
_deprecationRepository = deprecationRepository ?? throw new ArgumentNullException(nameof(deprecationRepository));
5457
_userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
5558
_scopeRepository = scopeRepository ?? throw new ArgumentNullException(nameof(scopeRepository));
@@ -111,6 +114,7 @@ private async Task DeleteAccountImplAsync(User userToBeDeleted, User userToExecu
111114
await RemoveSecurityPolicies(userToBeDeleted);
112115
await RemoveUserCredentials(userToBeDeleted);
113116
await RemovePackageOwnershipRequests(userToBeDeleted);
117+
ResetPackagesAndAccountsDeletedBy(userToBeDeleted);
114118

115119
RemovePackagePushedBy(userToBeDeleted);
116120
RemovePackageDeprecatedBy(userToBeDeleted);
@@ -318,6 +322,25 @@ private void RemoveMembers(Organization organization)
318322
}
319323
}
320324

325+
private void ResetPackagesAndAccountsDeletedBy(User user)
326+
{
327+
foreach (var deletedPackage in _packageDeleteRepository
328+
.GetAll()
329+
.Where(d => d.DeletedByKey == user.Key)
330+
.ToList())
331+
{
332+
deletedPackage.DeletedBy = null;
333+
}
334+
335+
foreach (var deletedAccount in _accountDeleteRepository
336+
.GetAll()
337+
.Where(d => d.DeletedByKey == user.Key)
338+
.ToList())
339+
{
340+
deletedAccount.DeletedBy = null;
341+
}
342+
}
343+
321344
private void RemoveUserDataInUserTable(User user)
322345
{
323346
user.SetAccountAsDeleted();

tests/NuGetGallery.Facts/Services/DeleteAccountServiceFacts.cs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ await deleteAccountService.DeleteAccountAsync(
141141
Assert.NotEmpty(testUser.OrganizationMigrationRequests);
142142
Assert.NotEmpty(testUser.OrganizationRequests);
143143
Assert.NotEmpty(testUser.Organizations);
144+
Assert.NotNull(testableService.PackageDeletedByUser.DeletedBy);
145+
Assert.NotNull(testableService.AccountDeletedByUser.DeletedBy);
144146
}
145147
else
146148
{
@@ -159,6 +161,8 @@ await deleteAccountService.DeleteAccountAsync(
159161
Assert.Null(testUser.OrganizationMigrationRequest);
160162
Assert.Empty(testUser.OrganizationMigrationRequests);
161163
Assert.Empty(testUser.OrganizationRequests);
164+
Assert.Null(testableService.PackageDeletedByUser.DeletedBy);
165+
Assert.Null(testableService.AccountDeletedByUser.DeletedBy);
162166

163167
Assert.Empty(testUser.Organizations);
164168
foreach (var testUserOrganization in testUserOrganizations)
@@ -185,6 +189,9 @@ await deleteAccountService.DeleteAccountAsync(
185189
// In production, they would not be deleted because the transaction they were deleted in would fail.
186190
Assert.Single(testableService.SupportRequests);
187191
Assert.Empty(testUser.ReservedNamespaces);
192+
193+
Assert.NotNull(testableService.PackageDeletedByDifferentUser.DeletedBy);
194+
Assert.NotNull(testableService.AccountDeletedByDifferentUser.DeletedBy);
188195
}
189196

190197
[Theory]
@@ -270,6 +277,8 @@ public async Task DeleteOrganization(bool isPackageOrphaned, AccountDeletionOrph
270277
Assert.Empty(testableService.AuditService.Records);
271278
Assert.False(testableService.HasDeletedOwnerScope);
272279
Assert.Empty(testableService.AuditService.Records);
280+
Assert.NotNull(testableService.PackageDeletedByUser.DeletedBy);
281+
Assert.NotNull(testableService.AccountDeletedByUser.DeletedBy);
273282
}
274283
else
275284
{
@@ -286,6 +295,8 @@ public async Task DeleteOrganization(bool isPackageOrphaned, AccountDeletionOrph
286295
Assert.Empty(testableService.PackageOwnerRequests);
287296
Assert.Single(testableService.AuditService.Records);
288297
Assert.True(testableService.HasDeletedOwnerScope);
298+
Assert.Null(testableService.PackageDeletedByUser.DeletedBy);
299+
Assert.Null(testableService.AccountDeletedByUser.DeletedBy);
289300

290301
var deleteRecord = testableService.AuditService.Records[0] as DeleteAccountAuditRecord;
291302
Assert.True(deleteRecord != null);
@@ -296,6 +307,9 @@ public async Task DeleteOrganization(bool isPackageOrphaned, AccountDeletionOrph
296307
// In production, they would not be deleted because the transaction they were deleted in would fail.
297308
Assert.Empty(organization.ReservedNamespaces);
298309
Assert.Single(testableService.SupportRequests);
310+
311+
Assert.NotNull(testableService.PackageDeletedByDifferentUser.DeletedBy);
312+
Assert.NotNull(testableService.AccountDeletedByDifferentUser.DeletedBy);
299313
}
300314

301315
[Theory]
@@ -473,7 +487,7 @@ public class DeleteAccountTestService
473487
private PackageRegistration _userPackagesRegistration = null;
474488
private ICollection<Package> _userPackages;
475489
private bool _hasDeletedCredentialWithOwnerScope = false;
476-
490+
477491
public List<AccountDelete> DeletedAccounts = new List<AccountDelete>();
478492
public List<User> DeletedUsers = new List<User>();
479493
public List<Issue> SupportRequests = new List<Issue>();
@@ -483,6 +497,12 @@ public class DeleteAccountTestService
483497
public FakeAuditingService AuditService = new FakeAuditingService();
484498
public bool HasDeletedOwnerScope => _hasDeletedCredentialWithOwnerScope;
485499

500+
public AccountDelete AccountDeletedByUser { get; }
501+
public AccountDelete AccountDeletedByDifferentUser { get; }
502+
public PackageDelete PackageDeletedByUser { get; }
503+
public PackageDelete PackageDeletedByDifferentUser { get; }
504+
505+
486506
public DeleteAccountTestService(User user)
487507
{
488508
_user = user;
@@ -533,6 +553,11 @@ public DeleteAccountTestService(User user, PackageRegistration userPackagesRegis
533553
NewOwner = _user
534554
});
535555

556+
AccountDeletedByUser = new AccountDelete { DeletedBy = _user, DeletedByKey = _user.Key };
557+
AccountDeletedByDifferentUser = new AccountDelete { DeletedBy = new User { Key = 1111 }, DeletedByKey = 1111 };
558+
PackageDeletedByUser = new PackageDelete { DeletedBy = _user, DeletedByKey = _user.Key };
559+
PackageDeletedByDifferentUser = new PackageDelete { DeletedBy = new User { Key = 1111 }, DeletedByKey = 1111 };
560+
536561
PackagePushedByUser = new Package
537562
{
538563
User = _user,
@@ -554,6 +579,7 @@ public DeleteAccountService GetDeleteAccountService(bool isPackageOrphaned, bool
554579
{
555580
return new DeleteAccountService(
556581
SetupAccountDeleteRepository().Object,
582+
SetupPackageDeleteRepository().Object,
557583
SetupDeprecationRepository().Object,
558584
SetupUserRepository().Object,
559585
SetupScopeRepository().Object,
@@ -639,13 +665,35 @@ private Mock<ISecurityPolicyService> SetupSecurityPolicyService()
639665
private Mock<IEntityRepository<AccountDelete>> SetupAccountDeleteRepository()
640666
{
641667
var accountDeleteRepository = new Mock<IEntityRepository<AccountDelete>>();
668+
669+
if (AccountDeletedByUser != null)
670+
{
671+
accountDeleteRepository
672+
.Setup(m => m.GetAll())
673+
.Returns(new[] { AccountDeletedByUser, AccountDeletedByDifferentUser }.AsQueryable());
674+
}
675+
642676
accountDeleteRepository
643677
.Setup(m => m.InsertOnCommit(It.IsAny<AccountDelete>()))
644678
.Callback<AccountDelete>(account => DeletedAccounts.Add(account));
645679

646680
return accountDeleteRepository;
647681
}
648682

683+
private Mock<IEntityRepository<PackageDelete>> SetupPackageDeleteRepository()
684+
{
685+
var packageDeleteRepository = new Mock<IEntityRepository<PackageDelete>>();
686+
687+
if (PackageDeletedByUser != null)
688+
{
689+
packageDeleteRepository
690+
.Setup(m => m.GetAll())
691+
.Returns(new[] { PackageDeletedByUser, PackageDeletedByDifferentUser }.AsQueryable());
692+
}
693+
694+
return packageDeleteRepository;
695+
}
696+
649697
private Mock<IEntityRepository<PackageDeprecation>> SetupDeprecationRepository()
650698
{
651699
var deprecationRepository = new Mock<IEntityRepository<PackageDeprecation>>();

0 commit comments

Comments
 (0)