Skip to content

Commit 33ef003

Browse files
author
Scott Bommarito
authored
DeleteAccountService should clean up Package.User and PackageDeprecation.DeprecatedBy (#7076)
1 parent a598acf commit 33ef003

8 files changed

Lines changed: 126 additions & 26 deletions

File tree

src/NuGet.Services.Entities/Package.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,11 @@ public bool HasReadMe
217217
public string MinClientVersion { get; set; }
218218

219219
/// <summary>
220-
/// The logged in user when this package version was created.
221-
/// NULL for older packages.
220+
/// The user that uploaded this package or <c>null</c> if the user was deleted.
222221
/// </summary>
222+
/// <remarks>
223+
/// Packages uploaded before this field was added have <c>null</c>.
224+
/// </remarks>
223225
public User User { get; set; }
224226
public int? UserKey { get; set; }
225227

src/NuGet.Services.Entities/PackageDeprecation.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ public PackageDeprecation()
7272
/// <summary>
7373
/// Gets or sets the user that executed the package deprecation.
7474
/// </summary>
75+
/// <remarks>
76+
/// This field will be <c>null</c> if the user that deprecated the package has been deleted.
77+
/// </remarks>
7578
public virtual User DeprecatedByUser { get; set; }
7679

7780
/// <summary>

src/NuGetGallery.Core/Entities/EntitiesContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public EntitiesContext(DbConnection connection, bool readOnly)
4949
}
5050

5151
public bool ReadOnly { get; private set; }
52+
public DbSet<Package> Packages { get; set; }
5253
public DbSet<PackageRegistration> PackageRegistrations { get; set; }
5354
public DbSet<Credential> Credentials { get; set; }
5455
public DbSet<Scope> Scopes { get; set; }

src/NuGetGallery.Core/Entities/IEntitiesContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace NuGetGallery
1010
public interface IEntitiesContext
1111
{
1212
DbSet<Certificate> Certificates { get; set; }
13+
DbSet<Package> Packages { get; set; }
1314
DbSet<PackageRegistration> PackageRegistrations { get; set; }
1415
DbSet<Credential> Credentials { get; set; }
1516
DbSet<Scope> Scopes { get; set; }

src/NuGetGallery/Services/DeleteAccountService.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Data.Entity;
76
using System.Globalization;
87
using System.Linq;
98
using System.Threading.Tasks;
@@ -26,6 +25,7 @@ public class DeleteAccountService : IDeleteAccountService
2625
private readonly IReservedNamespaceService _reservedNamespaceService;
2726
private readonly ISecurityPolicyService _securityPolicyService;
2827
private readonly AuthenticationService _authService;
28+
private readonly IEntityRepository<PackageDeprecation> _deprecationRepository;
2929
private readonly IEntityRepository<User> _userRepository;
3030
private readonly IEntityRepository<Scope> _scopeRepository;
3131
private readonly ISupportRequestService _supportRequestService;
@@ -35,6 +35,7 @@ public class DeleteAccountService : IDeleteAccountService
3535

3636
public DeleteAccountService(
3737
IEntityRepository<AccountDelete> accountDeleteRepository,
38+
IEntityRepository<PackageDeprecation> deprecationRepository,
3839
IEntityRepository<User> userRepository,
3940
IEntityRepository<Scope> scopeRepository,
4041
IEntitiesContext entitiesContext,
@@ -49,6 +50,7 @@ public DeleteAccountService(
4950
ITelemetryService telemetryService)
5051
{
5152
_accountDeleteRepository = accountDeleteRepository ?? throw new ArgumentNullException(nameof(accountDeleteRepository));
53+
_deprecationRepository = deprecationRepository ?? throw new ArgumentNullException(nameof(deprecationRepository));
5254
_userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
5355
_scopeRepository = scopeRepository ?? throw new ArgumentNullException(nameof(scopeRepository));
5456
_entitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext));
@@ -110,6 +112,9 @@ private async Task DeleteAccountImplAsync(User userToBeDeleted, User userToExecu
110112
await RemoveUserCredentials(userToBeDeleted);
111113
await RemovePackageOwnershipRequests(userToBeDeleted);
112114

115+
RemovePackagePushedBy(userToBeDeleted);
116+
RemovePackageDeprecatedBy(userToBeDeleted);
117+
113118
var organizationToBeDeleted = userToBeDeleted as Organization;
114119
if (organizationToBeDeleted != null)
115120
{
@@ -209,6 +214,17 @@ private async Task RemovePackageOwnership(User user, User requestingUser, Accoun
209214
}
210215
}
211216

217+
private void RemovePackagePushedBy(User user)
218+
{
219+
foreach (var package in _entitiesContext
220+
.Packages
221+
.Where(p => p.UserKey == user.Key)
222+
.ToList())
223+
{
224+
package.User = null;
225+
}
226+
}
227+
212228
private List<Package> GetPackagesOwnedByUser(User user)
213229
{
214230
return _packageService
@@ -218,15 +234,32 @@ private List<Package> GetPackagesOwnedByUser(User user)
218234

219235
private async Task RemovePackageOwnershipRequests(User user)
220236
{
221-
var requests = _packageOwnershipManagementService
237+
var toRequests = _packageOwnershipManagementService
222238
.GetPackageOwnershipRequests(newOwner: user)
223239
.ToList();
224240

241+
var fromRequests = _packageOwnershipManagementService
242+
.GetPackageOwnershipRequests(requestingOwner: user)
243+
.ToList();
244+
245+
var requests = toRequests.Concat(fromRequests).ToList();
246+
225247
foreach (var request in requests)
226248
{
227249
await _packageOwnershipManagementService.DeletePackageOwnershipRequestAsync(request.PackageRegistration, request.NewOwner, commitChanges: false);
228250
}
229251
}
252+
253+
private void RemovePackageDeprecatedBy(User user)
254+
{
255+
foreach (var deprecation in _deprecationRepository
256+
.GetAll()
257+
.Where(d => d.DeprecatedByUserKey == user.Key)
258+
.ToList())
259+
{
260+
deprecation.DeprecatedByUser = null;
261+
}
262+
}
230263

231264
private async Task RemoveMemberships(User user, User requestingUser, AccountDeletionOrphanPackagePolicy orphanPackagePolicy)
232265
{

src/NuGetGallery/Services/PackageService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ public async Task RemovePackageOwnerAsync(PackageRegistration package, User user
368368
{
369369
// To support the delete account scenario, the admin can delete the last owner of a package.
370370
package.Owners.Remove(user);
371+
371372
if (commitChanges)
372373
{
373374
await _packageRepository.CommitChangesAsync();

tests/NuGetGallery.Facts/Services/DeleteAccountServiceFacts.cs

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ await deleteAccountService.DeleteAccountAsync(
131131
Assert.NotEmpty(testUser.SecurityPolicies);
132132
Assert.True(registration.Packages.Single().Listed);
133133
Assert.NotNull(testUser.EmailAddress);
134+
Assert.NotNull(testableService.PackagePushedByUser.User);
135+
Assert.NotNull(testableService.DeprecationDeprecatedByUser.DeprecatedByUser);
134136
Assert.Empty(testableService.DeletedAccounts);
135137
Assert.NotEmpty(testableService.PackageOwnerRequests);
136138
Assert.False(testableService.HasDeletedOwnerScope);
@@ -148,6 +150,8 @@ await deleteAccountService.DeleteAccountAsync(
148150
orphanPolicy == AccountDeletionOrphanPackagePolicy.UnlistOrphans && isPackageOrphaned,
149151
!registration.Packages.Single().Listed);
150152
Assert.Null(testUser.EmailAddress);
153+
Assert.Null(testableService.PackagePushedByUser.User);
154+
Assert.Null(testableService.DeprecationDeprecatedByUser.DeprecatedByUser);
151155
Assert.Single(testableService.DeletedAccounts);
152156
Assert.Empty(testableService.PackageOwnerRequests);
153157
Assert.True(testableService.HasDeletedOwnerScope);
@@ -259,6 +263,8 @@ public async Task DeleteOrganization(bool isPackageOrphaned, AccountDeletionOrph
259263
Assert.Equal(organization.Confirmed, organization.EmailAddress != null);
260264
Assert.True(registration.Owners.Any(o => o.MatchesUser(organization)));
261265
Assert.NotEmpty(organization.SecurityPolicies);
266+
Assert.NotNull(testableService.PackagePushedByUser.User);
267+
Assert.NotNull(testableService.DeprecationDeprecatedByUser.DeprecatedByUser);
262268
Assert.Empty(testableService.DeletedAccounts);
263269
Assert.NotEmpty(testableService.PackageOwnerRequests);
264270
Assert.Empty(testableService.AuditService.Records);
@@ -274,6 +280,8 @@ public async Task DeleteOrganization(bool isPackageOrphaned, AccountDeletionOrph
274280
!registration.Packages.Single().Listed);
275281
Assert.False(registration.Owners.Any(o => o.MatchesUser(organization)));
276282
Assert.Empty(organization.SecurityPolicies);
283+
Assert.Null(testableService.PackagePushedByUser.User);
284+
Assert.Null(testableService.DeprecationDeprecatedByUser.DeprecatedByUser);
277285
Assert.Single(testableService.DeletedAccounts);
278286
Assert.Empty(testableService.PackageOwnerRequests);
279287
Assert.Single(testableService.AuditService.Records);
@@ -334,6 +342,8 @@ public async Task DeleteUnconfirmedOrganization(bool isPackageOrphaned)
334342
Assert.Empty(registration.Owners);
335343
Assert.Empty(organization.SecurityPolicies);
336344
Assert.Empty(organization.ReservedNamespaces);
345+
Assert.Null(testableService.PackagePushedByUser.User);
346+
Assert.Null(testableService.DeprecationDeprecatedByUser.DeprecatedByUser);
337347
Assert.Empty(testableService.DeletedAccounts);
338348
Assert.Single(testableService.SupportRequests);
339349
Assert.Empty(testableService.PackageOwnerRequests);
@@ -467,6 +477,8 @@ public class DeleteAccountTestService
467477
public List<AccountDelete> DeletedAccounts = new List<AccountDelete>();
468478
public List<User> DeletedUsers = new List<User>();
469479
public List<Issue> SupportRequests = new List<Issue>();
480+
public Package PackagePushedByUser;
481+
public PackageDeprecation DeprecationDeprecatedByUser;
470482
public List<PackageOwnerRequest> PackageOwnerRequests = new List<PackageOwnerRequest>();
471483
public FakeAuditingService AuditService = new FakeAuditingService();
472484
public bool HasDeletedOwnerScope => _hasDeletedCredentialWithOwnerScope;
@@ -520,6 +532,18 @@ public DeleteAccountTestService(User user, PackageRegistration userPackagesRegis
520532
PackageRegistration = new PackageRegistration() { Id = $"{user.Username}_second" },
521533
NewOwner = _user
522534
});
535+
536+
PackagePushedByUser = new Package
537+
{
538+
User = _user,
539+
UserKey = _user.Key
540+
};
541+
542+
DeprecationDeprecatedByUser = new PackageDeprecation
543+
{
544+
DeprecatedByUser = _user,
545+
DeprecatedByUserKey = _user.Key
546+
};
523547
}
524548

525549
public DeleteAccountTestService()
@@ -528,7 +552,9 @@ public DeleteAccountTestService()
528552

529553
public DeleteAccountService GetDeleteAccountService(bool isPackageOrphaned, bool isFeatureFlagsRemovalSuccessful = true)
530554
{
531-
return new DeleteAccountService(SetupAccountDeleteRepository().Object,
555+
return new DeleteAccountService(
556+
SetupAccountDeleteRepository().Object,
557+
SetupDeprecationRepository().Object,
532558
SetupUserRepository().Object,
533559
SetupScopeRepository().Object,
534560
SetupEntitiesContext().Object,
@@ -563,8 +589,24 @@ private Mock<IEntitiesContext> SetupEntitiesContext()
563589
{
564590
var mockContext = new Mock<IEntitiesContext>();
565591
var database = new Mock<IDatabase>();
566-
database.Setup(x => x.BeginTransaction()).Returns(() => new Mock<IDbContextTransaction>().Object);
567-
mockContext.Setup(m => m.GetDatabase()).Returns(database.Object);
592+
database
593+
.Setup(x => x.BeginTransaction())
594+
.Returns(() => new Mock<IDbContextTransaction>().Object);
595+
596+
mockContext
597+
.Setup(m => m.GetDatabase())
598+
.Returns(database.Object);
599+
600+
var packageDbSet = FakeEntitiesContext.CreateDbSet<Package>();
601+
mockContext
602+
.Setup(x => x.Packages)
603+
.Returns(packageDbSet);
604+
605+
if (PackagePushedByUser != null)
606+
{
607+
packageDbSet.Add(PackagePushedByUser);
608+
}
609+
568610
return mockContext;
569611
}
570612

@@ -574,8 +616,8 @@ private Mock<IReservedNamespaceService> SetupReservedNamespaceService()
574616
if (_user != null)
575617
{
576618
namespaceService.Setup(m => m.DeleteOwnerFromReservedNamespaceAsync(It.IsAny<string>(), It.IsAny<string>(), false))
577-
.Returns(Task.CompletedTask)
578-
.Callback(() => _user.ReservedNamespaces.Remove(_reservedNamespace));
619+
.Returns(Task.CompletedTask)
620+
.Callback(() => _user.ReservedNamespaces.Remove(_reservedNamespace));
579621
}
580622

581623
return namespaceService;
@@ -587,8 +629,8 @@ private Mock<ISecurityPolicyService> SetupSecurityPolicyService()
587629
if (_user != null)
588630
{
589631
securityPolicyService.Setup(m => m.UnsubscribeAsync(_user, SubscriptionName, false))
590-
.Returns(Task.CompletedTask)
591-
.Callback(() => _user.SecurityPolicies.Remove(_securityPolicy));
632+
.Returns(Task.CompletedTask)
633+
.Callback(() => _user.SecurityPolicies.Remove(_securityPolicy));
592634
}
593635

594636
return securityPolicyService;
@@ -597,11 +639,27 @@ private Mock<ISecurityPolicyService> SetupSecurityPolicyService()
597639
private Mock<IEntityRepository<AccountDelete>> SetupAccountDeleteRepository()
598640
{
599641
var accountDeleteRepository = new Mock<IEntityRepository<AccountDelete>>();
600-
accountDeleteRepository.Setup(m => m.InsertOnCommit(It.IsAny<AccountDelete>()))
601-
.Callback<AccountDelete>(account => DeletedAccounts.Add(account));
642+
accountDeleteRepository
643+
.Setup(m => m.InsertOnCommit(It.IsAny<AccountDelete>()))
644+
.Callback<AccountDelete>(account => DeletedAccounts.Add(account));
645+
602646
return accountDeleteRepository;
603647
}
604648

649+
private Mock<IEntityRepository<PackageDeprecation>> SetupDeprecationRepository()
650+
{
651+
var deprecationRepository = new Mock<IEntityRepository<PackageDeprecation>>();
652+
var deprecations = DeprecationDeprecatedByUser == null
653+
? Enumerable.Empty<PackageDeprecation>()
654+
: new[] { DeprecationDeprecatedByUser };
655+
656+
deprecationRepository
657+
.Setup(x => x.GetAll())
658+
.Returns(deprecations.AsQueryable());
659+
660+
return deprecationRepository;
661+
}
662+
605663
private Mock<IEntityRepository<User>> SetupUserRepository()
606664
{
607665
var userRepository = new Mock<IEntityRepository<User>>();
@@ -657,8 +715,8 @@ private Mock<IPackageService> SetupPackageService(bool isPackageOrphaned)
657715

658716
//the .Returns(Task.CompletedTask) to avoid NullRef exception by the Mock infrastructure when invoking async operations
659717
packageService.Setup(m => m.MarkPackageUnlistedAsync(It.IsAny<Package>(), false))
660-
.Returns(Task.CompletedTask)
661-
.Callback<Package, bool>((package, commit) => { package.Listed = false; });
718+
.Returns(Task.CompletedTask)
719+
.Callback<Package, bool>((package, commit) => { package.Listed = false; });
662720

663721
return packageService;
664722
}
@@ -689,8 +747,8 @@ private Mock<ISupportRequestService> SetupSupportRequestService()
689747
{
690748
var issue = SupportRequests.Where(i => string.Equals(i.CreatedBy, _user.Username)).FirstOrDefault();
691749
supportService.Setup(m => m.DeleteSupportRequestsAsync(_user))
692-
.Returns(Task.FromResult(true))
693-
.Callback(() => SupportRequests.Remove(issue));
750+
.Returns(Task.FromResult(true))
751+
.Callback(() => SupportRequests.Remove(issue));
694752
}
695753

696754
return supportService;
@@ -715,13 +773,14 @@ private Mock<IPackageOwnershipManagementService> SetupPackageOwnershipManagement
715773
var packageOwnershipManagementService = new Mock<IPackageOwnershipManagementService>();
716774
if (_user != null)
717775
{
718-
packageOwnershipManagementService.Setup(m => m.RemovePackageOwnerAsync(It.IsAny<PackageRegistration>(), It.IsAny<User>(), It.IsAny<User>(), false))
719-
.Returns(Task.CompletedTask)
720-
.Callback(() =>
721-
{
722-
_userPackagesRegistration.Owners.Remove(_user);
723-
_userPackagesRegistration.ReservedNamespaces.Remove(_reservedNamespace);
724-
});
776+
packageOwnershipManagementService
777+
.Setup(m => m.RemovePackageOwnerAsync(It.IsAny<PackageRegistration>(), It.IsAny<User>(), It.IsAny<User>(), false))
778+
.Returns(Task.CompletedTask)
779+
.Callback(() =>
780+
{
781+
_userPackagesRegistration.Owners.Remove(_user);
782+
_userPackagesRegistration.ReservedNamespaces.Remove(_reservedNamespace);
783+
});
725784

726785
packageOwnershipManagementService.Setup(m => m.GetPackageOwnershipRequests(null, null, _user))
727786
.Returns(PackageOwnerRequests);

tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,7 +1986,7 @@ public async Task EnsureValidThrowsForSymbolsPackage()
19861986
public class TheRemovePackageOwnerMethod
19871987
{
19881988
[Fact]
1989-
public async Task RemovesPackageOwner()
1989+
public async Task WillRemoveOneOfManyOwners()
19901990
{
19911991
var service = CreateService();
19921992
var owner1 = new User { Key = 1, Username = "Owner1" };
@@ -1999,7 +1999,7 @@ public async Task RemovesPackageOwner()
19991999
}
20002000

20012001
[Fact]
2002-
public async Task WontRemoveLastOwner()
2002+
public async Task WillRemoveLastOwner()
20032003
{
20042004
var service = CreateService();
20052005
var singleOwner = new User { Key = 1, Username = "Owner" };

0 commit comments

Comments
 (0)