Skip to content

Commit 57e2420

Browse files
author
Scott Bommarito
authored
Improve the logic for how we determine whether or not packages will be orphaned by an account deletion. (#6714)
1 parent 4a46fa0 commit 57e2420

22 files changed

Lines changed: 429 additions & 673 deletions

src/NuGetGallery/Areas/Admin/ViewModels/DeleteAccountAsAdminViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public DeleteAccountAsAdminViewModel()
1515
public DeleteAccountAsAdminViewModel(IDeleteAccountViewModel model)
1616
{
1717
AccountName = model.AccountName;
18-
HasOrphanPackages = model.HasOrphanPackages;
18+
HasPackagesThatWillBeOrphaned = model.HasPackagesThatWillBeOrphaned;
1919
}
2020

2121
public string AccountName { get; set; }
@@ -28,6 +28,6 @@ public DeleteAccountAsAdminViewModel(IDeleteAccountViewModel model)
2828
[Display(Name = "Unlist any orphaned packages.")]
2929
public bool ShouldUnlist { get; set; }
3030

31-
public bool HasOrphanPackages { get; set; }
31+
public bool HasPackagesThatWillBeOrphaned { get; set; }
3232
}
3333
}

src/NuGetGallery/Areas/Admin/Views/DeleteAccount/_DeleteUserAccountForm.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
@Html.ValidationMessageFor(m => m.Signature)
1414
</div>
1515

16-
@if (Model.HasOrphanPackages)
16+
@if (Model.HasPackagesThatWillBeOrphaned)
1717
{
1818
<div class="form-group">
1919
@Html.EditorFor(m => m.ShouldUnlist)

src/NuGetGallery/Controllers/OrganizationsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ public override async Task<ActionResult> RequestAccountDeletion(string accountNa
353353

354354
var model = GetDeleteOrganizationViewModel(account);
355355

356-
if (model.HasOrphanPackages)
356+
if (model.HasPackagesThatWillBeOrphaned)
357357
{
358358
TempData["ErrorMessage"] = "You cannot delete your organization unless you transfer ownership of all of its packages to another account.";
359359

src/NuGetGallery/Extensions/OrganizationExtensions.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,5 @@ public static Membership GetMembershipOfUser(this Organization organization, Use
1313
{
1414
return organization?.Members?.FirstOrDefault(m => m.Member.MatchesUser(member));
1515
}
16-
17-
/// <summary>
18-
/// Returns all the user accounts that are members of an organization.
19-
/// If the organization has nested organizations their members will be returned as well.
20-
/// The result will not have duplicate elements.
21-
///
22-
/// Nested organizations (teams) are not supported in the Gallery yet, but this method allows for it in case we lift that constraint.
23-
/// </summary>
24-
/// <param name="organization">The organization.</param>
25-
/// <returns>The <see cref="IEnumerable{User}"/> of users that are not <see cref="Organization"/> and are members of <paramref name="organization"/>.</returns>
26-
public static IEnumerable<User> GetUserAccountMembers(this Organization organization)
27-
{
28-
Queue<Organization> organizations = new Queue<Organization>();
29-
organizations.Enqueue(organization);
30-
HashSet<User> distinctUsers = new HashSet<User>();
31-
while (organizations.Any())
32-
{
33-
var currentOrganization = organizations.Dequeue();
34-
foreach (var membership in currentOrganization.Members)
35-
{
36-
if (membership.Member is Organization)
37-
{
38-
organizations.Enqueue((Organization)membership.Member);
39-
}
40-
else
41-
{
42-
distinctUsers.Add(membership.Member);
43-
}
44-
}
45-
}
46-
47-
return distinctUsers;
48-
}
4916
}
5017
}

src/NuGetGallery/Services/DeleteAccountService.cs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,8 @@ private async Task RemovePackageOwnership(User user, User requestingUser, Accoun
183183
{
184184
foreach (var package in GetPackagesOwnedByUser(user))
185185
{
186-
var owners = user is Organization ? package.PackageRegistration.Owners : _packageService.GetPackageUserAccountOwners(package);
187-
if (owners.Count() <= 1)
186+
if (_packageService.WillPackageBeOrphanedIfOwnerRemoved(package.PackageRegistration, user))
188187
{
189-
// Package will be orphaned by removing ownership.
190188
if (orphanPackagePolicy == AccountDeletionOrphanPackagePolicy.DoNotAllowOrphans)
191189
{
192190
throw new InvalidOperationException($"Deleting user '{user.Username}' will make package '{package.PackageRegistration.Id}' an orphan, but no orphans were expected.");
@@ -201,12 +199,6 @@ private async Task RemovePackageOwnership(User user, User requestingUser, Accoun
201199
}
202200
}
203201

204-
private bool WillPackageBeOrphaned(User user, Package package)
205-
{
206-
var owners = user is Organization ? package.PackageRegistration.Owners : _packageService.GetPackageUserAccountOwners(package);
207-
return owners.Count() <= 1;
208-
}
209-
210202
private List<Package> GetPackagesOwnedByUser(User user)
211203
{
212204
return _packageService.FindPackagesByAnyMatchingOwner(user, includeUnlisted: true, includeVersions: true).ToList();
@@ -225,22 +217,21 @@ private async Task RemoveMemberships(User user, User requestingUser, AccountDele
225217
{
226218
foreach (var membership in user.Organizations.ToArray())
227219
{
228-
var organization = membership.Organization;
229-
var members = organization.Members.ToList();
230-
var collaborators = members.Where(m => !m.IsAdmin).ToList();
231-
var memberCount = members.Count();
232220
user.Organizations.Remove(membership);
221+
var organization = membership.Organization;
222+
var otherMembers = organization.Members
223+
.Where(m => !m.Member.MatchesUser(user));
233224

234-
if (memberCount < 2)
225+
if (!otherMembers.Any())
235226
{
236227
// The user we are deleting is the only member of the organization.
237228
// We should delete the entire organization.
238229
await DeleteAccountImplAsync(organization, requestingUser, orphanPackagePolicy);
239230
}
240-
else if (memberCount - 1 <= collaborators.Count())
231+
else if (otherMembers.All(m => !m.IsAdmin))
241232
{
242233
// All other members of this organization are collaborators, so we should promote them to administrators.
243-
foreach (var collaborator in collaborators)
234+
foreach (var collaborator in otherMembers)
244235
{
245236
collaborator.IsAdmin = true;
246237
}

src/NuGetGallery/Services/IPackageService.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public interface IPackageService : ICorePackageService
7373

7474
Task RemovePackageOwnerAsync(PackageRegistration package, User user, bool commitChanges = true);
7575

76+
/// <remarks>
77+
/// A package is orphaned if it is not owned by a user account or an organization with user account members.
78+
/// </remarks>
79+
bool WillPackageBeOrphanedIfOwnerRemoved(PackageRegistration package, User ownerToRemove);
80+
7681
Task SetLicenseReportVisibilityAsync(Package package, bool visible, bool commitChanges = true);
7782

7883
Task EnsureValid(PackageArchiveReader packageArchiveReader);
@@ -81,13 +86,6 @@ public interface IPackageService : ICorePackageService
8186

8287
Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection<PackageRegistration> package, bool isVerified, bool commitChanges = true);
8388

84-
/// <summary>
85-
/// For a package get the list of owners that are not organizations.
86-
/// </summary>
87-
/// <param name="package">The package.</param>
88-
/// <returns>The list of package owners that are not organizations.</returns>
89-
IEnumerable<User> GetPackageUserAccountOwners(Package package);
90-
9189
/// <summary>
9290
/// Sets the required signer on all owned package registrations.
9391
/// </summary>

src/NuGetGallery/Services/PackageService.cs

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -282,26 +282,6 @@ private IEnumerable<Package> GetPackagesForOwners(IEnumerable<int> ownerKeys, bo
282282
.ToList();
283283
}
284284

285-
/// <summary>
286-
/// For a package get the list of owners that are not organizations.
287-
/// All the resulted user accounts will be distinct.
288-
/// </summary>
289-
/// <param name="package">The package.</param>
290-
/// <returns>The list of package owners.</returns>
291-
public IEnumerable<User> GetPackageUserAccountOwners(Package package)
292-
{
293-
return package.PackageRegistration.Owners
294-
.SelectMany((owner) =>
295-
{
296-
if (owner is Organization)
297-
{
298-
return OrganizationExtensions.GetUserAccountMembers((Organization)owner);
299-
}
300-
return new List<User> { owner };
301-
})
302-
.Distinct();
303-
}
304-
305285
public IQueryable<PackageRegistration> FindPackageRegistrationsByOwner(User user)
306286
{
307287
return _packageRegistrationRepository.GetAll().Where(p => p.Owners.Any(o => o.Key == user.Key));
@@ -377,6 +357,43 @@ public async Task RemovePackageOwnerAsync(PackageRegistration package, User user
377357
await _packageRepository.CommitChangesAsync();
378358
}
379359
}
360+
361+
public bool WillPackageBeOrphanedIfOwnerRemoved(PackageRegistration package, User ownerToRemove)
362+
{
363+
return WillPackageBeOrphanedIfOwnerRemovedHelper(package.Owners, ownerToRemove);
364+
}
365+
366+
private bool WillPackageBeOrphanedIfOwnerRemovedHelper(IEnumerable<User> owners, User ownerToRemove)
367+
{
368+
// Iterate through each owner, attempting to find a user that is not the owner we are removing.
369+
foreach (var owner in owners)
370+
{
371+
if (owner.MatchesUser(ownerToRemove))
372+
{
373+
continue;
374+
}
375+
376+
if (owner is Organization organization)
377+
{
378+
// The package will still be orphaned if it is owned by an orphaned organization.
379+
// Iterate through the organization owner's members to determine if it has any members that are not the member we are removing.
380+
if (!WillPackageBeOrphanedIfOwnerRemovedHelper(
381+
organization.Members.Select(m => m.Member),
382+
ownerToRemove))
383+
{
384+
return false;
385+
}
386+
}
387+
else
388+
{
389+
// The package will not be orphaned because it is owned by a user that is not the owner we are removing.
390+
return false;
391+
}
392+
}
393+
394+
// The package will be orphaned because we did not find an owner that is not the owner we are removing.
395+
return true;
396+
}
380397

381398
public async Task MarkPackageListedAsync(Package package, bool commitChanges = true)
382399
{

src/NuGetGallery/ViewModels/DeleteAccountViewModel.cs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ public class DeleteAccountViewModel<TAccount> : DeleteAccountViewModel where TAc
1313
public DeleteAccountViewModel(
1414
TAccount accountToDelete,
1515
User currentUser,
16-
IPackageService packageService,
17-
Func<ListPackageItemViewModel, bool> packageIsOrphaned)
18-
: base(accountToDelete, currentUser, packageService, packageIsOrphaned)
16+
IPackageService packageService)
17+
: base(accountToDelete, currentUser, packageService)
1918
{
2019
Account = accountToDelete;
2120
}
@@ -25,43 +24,49 @@ public DeleteAccountViewModel(
2524

2625
public class DeleteAccountViewModel : IDeleteAccountViewModel
2726
{
28-
private Lazy<bool> _hasOrphanPackages;
29-
3027
public DeleteAccountViewModel(
3128
User userToDelete,
3229
User currentUser,
33-
IPackageService packageService,
34-
Func<ListPackageItemViewModel, bool> packageIsOrphaned)
30+
IPackageService packageService)
3531
{
3632
User = userToDelete;
3733

3834
Packages = packageService
3935
.FindPackagesByAnyMatchingOwner(User, includeUnlisted: true)
40-
.Select(p => new ListPackageItemViewModel(p, currentUser))
36+
.Select(p => new DeleteAccountListPackageItemViewModel(p, userToDelete, currentUser, packageService))
4137
.ToList();
4238

43-
_hasOrphanPackages = new Lazy<bool>(() => Packages.Any(packageIsOrphaned));
39+
HasPackagesThatWillBeOrphaned = Packages.Any(p => p.WillBeOrphaned);
4440
}
4541

46-
public List<ListPackageItemViewModel> Packages { get; }
42+
public List<DeleteAccountListPackageItemViewModel> Packages { get; }
4743

4844
public User User { get; }
4945

5046
public string AccountName => User.Username;
5147

52-
public bool HasOrphanPackages
48+
public bool HasPackagesThatWillBeOrphaned { get; }
49+
}
50+
51+
public class DeleteAccountListPackageItemViewModel : ListPackageItemViewModel
52+
{
53+
public DeleteAccountListPackageItemViewModel(
54+
Package package,
55+
User userToDelete,
56+
User currentUser,
57+
IPackageService packageService)
58+
: base(package, currentUser)
5359
{
54-
get
55-
{
56-
return Packages == null ? false : _hasOrphanPackages.Value;
57-
}
60+
WillBeOrphaned = packageService.WillPackageBeOrphanedIfOwnerRemoved(package.PackageRegistration, userToDelete);
5861
}
62+
63+
public bool WillBeOrphaned { get; }
5964
}
6065

6166
public interface IDeleteAccountViewModel
6267
{
6368
string AccountName { get; }
6469

65-
bool HasOrphanPackages { get; }
70+
bool HasPackagesThatWillBeOrphaned { get; }
6671
}
6772
}

src/NuGetGallery/ViewModels/DeleteOrganizationViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public DeleteOrganizationViewModel(
1313
Organization organizationToDelete,
1414
User currentUser,
1515
IPackageService packageService)
16-
: base(organizationToDelete, currentUser, packageService, p => p.HasSingleOrganizationOwner)
16+
: base(organizationToDelete, currentUser, packageService)
1717
{
1818
AdditionalMembers = organizationToDelete.Members
1919
.Where(m => !m.Member.MatchesUser(currentUser))

src/NuGetGallery/ViewModels/DeleteUserViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public DeleteUserViewModel(
1616
User currentUser,
1717
IPackageService packageService,
1818
ISupportRequestService supportRequestService)
19-
: base(userToDelete, currentUser, packageService, p => p.HasSingleUserOwner)
19+
: base(userToDelete, currentUser, packageService)
2020
{
2121
Organizations = userToDelete.Organizations
2222
.Select(u => new ManageOrganizationsItemViewModel(u, packageService));

0 commit comments

Comments
 (0)