Skip to content

Commit 62a2866

Browse files
author
Scott Bommarito
authored
Allow organization admins to remove their organization's ownership of a package in a reserved namespace (#5984)
1 parent 8566f4a commit 62a2866

9 files changed

+196
-147
lines changed

src/NuGetGallery/Services/ActionRequiringReservedNamespacePermissions.cs

+28-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ namespace NuGetGallery
1515
/// E.g. "JQuery.Extensions.MyCoolExtension" matches both "JQuery.*" and "JQuery.Extensions.*".
1616
/// </remarks>
1717
public class ActionRequiringReservedNamespacePermissions
18-
: ActionRequiringEntityPermissions<IReadOnlyCollection<ReservedNamespace>>, IActionRequiringEntityPermissions<ActionOnNewPackageContext>
18+
: ActionRequiringEntityPermissions<IReadOnlyCollection<ReservedNamespace>>,
19+
IActionRequiringEntityPermissions<ActionOnNewPackageContext>,
20+
IActionRequiringEntityPermissions<ReservedNamespace>
1921
{
2022
public PermissionsRequirement ReservedNamespacePermissionsRequirement { get; }
2123

@@ -32,11 +34,21 @@ public PermissionsCheckResult CheckPermissions(User currentUser, User account, A
3234
return CheckPermissions(currentUser, account, GetReservedNamespaces(newPackageContext));
3335
}
3436

37+
public PermissionsCheckResult CheckPermissions(User currentUser, User account, ReservedNamespace reservedNamespace)
38+
{
39+
return CheckPermissions(currentUser, account, GetReservedNamespaces(reservedNamespace));
40+
}
41+
3542
public PermissionsCheckResult CheckPermissions(IPrincipal currentPrincipal, User account, ActionOnNewPackageContext newPackageContext)
3643
{
3744
return CheckPermissions(currentPrincipal, account, GetReservedNamespaces(newPackageContext));
3845
}
3946

47+
public PermissionsCheckResult CheckPermissions(IPrincipal currentPrincipal, User account, ReservedNamespace reservedNamespace)
48+
{
49+
return CheckPermissions(currentPrincipal, account, GetReservedNamespaces(reservedNamespace));
50+
}
51+
4052
protected override PermissionsCheckResult CheckPermissionsForEntity(User account, IReadOnlyCollection<ReservedNamespace> reservedNamespaces)
4153
{
4254
if (!reservedNamespaces.Any())
@@ -54,11 +66,21 @@ public PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(User currentU
5466
return CheckPermissionsOnBehalfOfAnyAccount(currentUser, GetReservedNamespaces(newPackageContext));
5567
}
5668

69+
public PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(User currentUser, ReservedNamespace reservedNamespace)
70+
{
71+
return CheckPermissionsOnBehalfOfAnyAccount(currentUser, GetReservedNamespaces(reservedNamespace));
72+
}
73+
5774
public PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(User currentUser, ActionOnNewPackageContext newPackageContext, out IEnumerable<User> accountsAllowedOnBehalfOf)
5875
{
5976
return CheckPermissionsOnBehalfOfAnyAccount(currentUser, GetReservedNamespaces(newPackageContext), out accountsAllowedOnBehalfOf);
6077
}
6178

79+
public PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(User currentUser, ReservedNamespace reservedNamespace, out IEnumerable<User> accountsAllowedOnBehalfOf)
80+
{
81+
return CheckPermissionsOnBehalfOfAnyAccount(currentUser, GetReservedNamespaces(reservedNamespace), out accountsAllowedOnBehalfOf);
82+
}
83+
6284
protected override IEnumerable<User> GetOwners(IReadOnlyCollection<ReservedNamespace> reservedNamespaces)
6385
{
6486
return reservedNamespaces.Any() ? reservedNamespaces.SelectMany(rn => rn.Owners) : Enumerable.Empty<User>();
@@ -68,5 +90,10 @@ private IReadOnlyCollection<ReservedNamespace> GetReservedNamespaces(ActionOnNew
6890
{
6991
return newPackageContext.ReservedNamespaceService.GetReservedNamespacesForId(newPackageContext.PackageId);
7092
}
93+
94+
private IReadOnlyCollection<ReservedNamespace> GetReservedNamespaces(ReservedNamespace reservedNamespace)
95+
{
96+
return new[] { reservedNamespace };
97+
}
7198
}
7299
}

src/NuGetGallery/Services/ActionsRequiringPermissions.cs

+16
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,21 @@ public static class ActionsRequiringPermissions
128128
new ActionRequiringPackagePermissions(
129129
accountOnBehalfOfPermissionsRequirement: RequireOwnerOrOrganizationAdmin,
130130
packageRegistrationPermissionsRequirement: RequireOwnerOrOrganizationAdmin);
131+
132+
/// <summary>
133+
/// The action of adding a package to a reserved namespace that the package is in.
134+
/// </summary>
135+
public static ActionRequiringReservedNamespacePermissions AddPackageToReservedNamespace =
136+
new ActionRequiringReservedNamespacePermissions(
137+
accountOnBehalfOfPermissionsRequirement: PermissionsRequirement.Owner,
138+
reservedNamespacePermissionsRequirement: PermissionsRequirement.Owner);
139+
140+
/// <summary>
141+
/// The action of removing a package from a reserved namespace that the package is in.
142+
/// </summary>
143+
public static ActionRequiringReservedNamespacePermissions RemovePackageFromReservedNamespace =
144+
new ActionRequiringReservedNamespacePermissions(
145+
accountOnBehalfOfPermissionsRequirement: RequireOwnerOrSiteAdminOrOrganizationAdmin,
146+
reservedNamespacePermissionsRequirement: RequireOwnerOrOrganizationAdmin);
131147
}
132148
}

src/NuGetGallery/Services/IPackageService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ public interface IPackageService : ICorePackageService
6868
/// <returns>Awaitable task.</returns>
6969
Task AddPackageOwnerAsync(PackageRegistration package, User newOwner);
7070

71-
Task RemovePackageOwnerAsync(PackageRegistration package, User user);
71+
Task RemovePackageOwnerAsync(PackageRegistration package, User user, bool commitChanges = true);
7272

7373
Task SetLicenseReportVisibilityAsync(Package package, bool visible, bool commitChanges = true);
7474

7575
void EnsureValid(PackageArchiveReader packageArchiveReader);
7676

7777
Task IncrementDownloadCountAsync(string id, string version, bool commitChanges = true);
7878

79-
Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection<PackageRegistration> package, bool isVerified);
79+
Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection<PackageRegistration> package, bool isVerified, bool commitChanges = true);
8080

8181
/// <summary>
8282
/// For a package get the list of owners that are not organizations.

src/NuGetGallery/Services/IReservedNamespaceService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ public interface IReservedNamespaceService
5757
/// Remove the specified package registration from the reserved namespace. It is the caller's reponsibility to
5858
/// commit the changes to the database.
5959
/// </summary>
60-
/// <param name="prefix">The prefix value of the reserved namespace to modify</param>
60+
/// <param name="reservedNamespace">The reserved namespace to modify</param>
6161
/// <param name="packageRegistration">The package registration entity to be removed.</param>
62-
void RemovePackageRegistrationFromNamespace(string prefix, PackageRegistration packageRegistration);
62+
void RemovePackageRegistrationFromNamespace(ReservedNamespace reservedNamespace, PackageRegistration packageRegistration);
6363

6464
/// <summary>
6565
/// Retrieves the first reserved namespace which matches the given prefix.

src/NuGetGallery/Services/PackageOwnershipManagementService.cs

+29-48
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ public async Task RemovePackageOwnerAsync(PackageRegistration packageRegistratio
121121
using (var strategy = new SuspendDbExecutionStrategy())
122122
using (var transaction = _entitiesContext.GetDatabase().BeginTransaction())
123123
{
124-
await RemovePackageOwnerImplAsync(packageRegistration, requestingOwner, ownerToBeRemoved);
124+
await RemovePackageOwnerImplAsync(packageRegistration, ownerToBeRemoved);
125125
transaction.Commit();
126126
}
127127
}
128128
else
129129
{
130-
await RemovePackageOwnerImplAsync(packageRegistration, requestingOwner, ownerToBeRemoved);
130+
await RemovePackageOwnerImplAsync(packageRegistration, ownerToBeRemoved);
131131
}
132132

133133
await _auditingService.SaveAuditRecordAsync(
@@ -139,38 +139,29 @@ await _auditingService.SaveAuditRecordAsync(
139139
}
140140
}
141141

142-
private async Task RemovePackageOwnerImplAsync(PackageRegistration packageRegistration, User requestingOwner, User ownerToBeRemoved)
142+
private async Task RemovePackageOwnerImplAsync(PackageRegistration packageRegistration, User ownerToBeRemoved)
143143
{
144-
// 1. Remove this package registration from the namespaces owned by this user if he is the only package owner in the set of matching namespaces
145-
// 2. Remove the IsVerified flag from package registration if all the matching namespaces are owned by this user alone (no other package owner owns a matching namespace for this PR)
146-
var allMatchingNamespacesForRegistration = packageRegistration.ReservedNamespaces;
147-
if (allMatchingNamespacesForRegistration.Any())
144+
// Remove the user from owners list of package registration
145+
await _packageService.RemovePackageOwnerAsync(packageRegistration, ownerToBeRemoved, commitChanges: false);
146+
147+
// Remove this package registration from the namespaces owned by this user that are owned by no other package owners
148+
foreach (var reservedNamespace in packageRegistration.ReservedNamespaces.ToArray())
148149
{
149-
var allPackageOwners = packageRegistration.Owners;
150-
var matchingNamespacesOwnedByUser = allMatchingNamespacesForRegistration
151-
.Where(rn => rn.Owners.Any(o => o == ownerToBeRemoved));
152-
var namespacesToModify = matchingNamespacesOwnedByUser
153-
.Where(rn => rn.Owners.Intersect(allPackageOwners).Count() == 1)
154-
.ToList();
155-
156-
if (namespacesToModify.Any())
150+
if (!packageRegistration.Owners
151+
.Any(o => ActionsRequiringPermissions.AddPackageToReservedNamespace
152+
.CheckPermissionsOnBehalfOfAnyAccount(o, reservedNamespace) == PermissionsCheckResult.Allowed))
157153
{
158-
// The package will lose its 'IsVerified' flag if the user is the only package owner who owns all the namespaces that match this registration
159-
var shouldModifyIsVerified = allMatchingNamespacesForRegistration.Count() == namespacesToModify.Count();
160-
if (shouldModifyIsVerified && packageRegistration.IsVerified)
161-
{
162-
await _packageService.UpdatePackageVerifiedStatusAsync(new List<PackageRegistration> { packageRegistration }, isVerified: false);
163-
}
164-
165-
namespacesToModify
166-
.ForEach(rn => _reservedNamespaceService.RemovePackageRegistrationFromNamespace(rn.Value, packageRegistration));
167-
168-
await _entitiesContext.SaveChangesAsync();
154+
_reservedNamespaceService.RemovePackageRegistrationFromNamespace(reservedNamespace, packageRegistration);
169155
}
170156
}
171157

172-
// Remove the user from owners list of package registration
173-
await _packageService.RemovePackageOwnerAsync(packageRegistration, ownerToBeRemoved);
158+
// Remove the IsVerified flag from package registration if all the matching namespaces are owned by this user alone (no other package owner owns a matching namespace for this PR)
159+
if (packageRegistration.IsVerified && !packageRegistration.ReservedNamespaces.Any())
160+
{
161+
await _packageService.UpdatePackageVerifiedStatusAsync(new List<PackageRegistration> { packageRegistration }, isVerified: false, commitChanges: false);
162+
}
163+
164+
await _entitiesContext.SaveChangesAsync();
174165
}
175166

176167
public async Task DeletePackageOwnershipRequestAsync(PackageRegistration packageRegistration, User newOwner)
@@ -191,31 +182,21 @@ public async Task DeletePackageOwnershipRequestAsync(PackageRegistration package
191182
await _packageOwnerRequestService.DeletePackageOwnershipRequest(request);
192183
}
193184
}
194-
195-
// The requesting owner can remove other owner only if
196-
// 1. Is an admin.
197-
// 2. Owns a namespace.
198-
// 3. Or the other user also does not own a namespace.
185+
199186
private static bool OwnerHasPermissionsToRemove(User requestingOwner, User ownerToBeRemoved, PackageRegistration packageRegistration)
200187
{
201-
if (requestingOwner.IsAdministrator)
188+
var reservedNamespaces = packageRegistration.ReservedNamespaces.ToList();
189+
if (ActionsRequiringPermissions.AddPackageToReservedNamespace
190+
.CheckPermissionsOnBehalfOfAnyAccount(ownerToBeRemoved, reservedNamespaces) == PermissionsCheckResult.Allowed)
202191
{
203-
return true;
192+
// If the owner to be removed owns a reserved namespace that applies to this package,
193+
// the requesting user must own a reserved namespace that applies to this package or be a site admin.
194+
return ActionsRequiringPermissions.RemovePackageFromReservedNamespace
195+
.CheckPermissionsOnBehalfOfAnyAccount(requestingOwner, reservedNamespaces) == PermissionsCheckResult.Allowed;
204196
}
205197

206-
var requestingOwnerOwnsNamespace = IsUserAnOwnerOfPackageNamespace(packageRegistration, requestingOwner);
207-
if (requestingOwnerOwnsNamespace)
208-
{
209-
return true;
210-
}
211-
212-
var ownerToBeRemovedOwnsNamespace = IsUserAnOwnerOfPackageNamespace(packageRegistration, ownerToBeRemoved);
213-
return !ownerToBeRemovedOwnsNamespace;
214-
}
215-
216-
private static bool IsUserAnOwnerOfPackageNamespace(PackageRegistration packageRegistration, User user)
217-
{
218-
return packageRegistration.ReservedNamespaces.Any(rn => rn.Owners.Any(owner => owner == user));
198+
// If the owner to be removed does not own any reserved namespaces that apply to this package, they can be removed by anyone.
199+
return true;
219200
}
220201
}
221202
}

src/NuGetGallery/Services/PackageService.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,14 @@ public async Task AddPackageOwnerAsync(PackageRegistration package, User newOwne
361361
}
362362
}
363363

364-
public async Task RemovePackageOwnerAsync(PackageRegistration package, User user)
364+
public async Task RemovePackageOwnerAsync(PackageRegistration package, User user, bool commitChanges = true)
365365
{
366366
// To support the delete account scenario, the admin can delete the last owner of a package.
367367
package.Owners.Remove(user);
368-
await _packageRepository.CommitChangesAsync();
368+
if (commitChanges)
369+
{
370+
await _packageRepository.CommitChangesAsync();
371+
}
369372
}
370373

371374
public async Task MarkPackageListedAsync(Package package, bool commitChanges = true)
@@ -701,7 +704,7 @@ public async Task IncrementDownloadCountAsync(string id, string version, bool co
701704
}
702705
}
703706

704-
public virtual async Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection<PackageRegistration> packageRegistrationList, bool isVerified)
707+
public virtual async Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection<PackageRegistration> packageRegistrationList, bool isVerified, bool commitChanges = true)
705708
{
706709
var packageRegistrationIdSet = new HashSet<string>(packageRegistrationList.Select(prl => prl.Id));
707710
var allPackageRegistrations = _packageRegistrationRepository.GetAll();
@@ -714,7 +717,10 @@ public virtual async Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection<P
714717
packageRegistrationsToUpdate
715718
.ForEach(pru => pru.IsVerified = isVerified);
716719

717-
await _packageRegistrationRepository.CommitChangesAsync();
720+
if (commitChanges)
721+
{
722+
await _packageRegistrationRepository.CommitChangesAsync();
723+
}
718724
}
719725
}
720726

0 commit comments

Comments
 (0)