Skip to content

Commit 4c38bb4

Browse files
author
Scott Bommarito
authored
Allow site admins to delete organizations they are not a part of (#7041)
1 parent 4f33488 commit 4c38bb4

19 files changed

Lines changed: 445 additions & 213 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace NuGetGallery.Areas.Admin.ViewModels
55
{
6-
public class DeleteUserAccountStatus
6+
public class DeleteAccountStatus
77
{
88
public bool Success { get; set; }
99

src/NuGetGallery/Areas/Admin/Views/DeleteAccount/DeleteUserAccountStatus.cshtml renamed to src/NuGetGallery/Areas/Admin/Views/DeleteAccount/DeleteAccountStatus.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@using NuGetGallery
2-
@model NuGetGallery.Areas.Admin.ViewModels.DeleteUserAccountStatus
2+
@model NuGetGallery.Areas.Admin.ViewModels.DeleteAccountStatus
33
@{
44
ViewBag.Title = "Delete Account Status" + Model.AccountName;
55
ViewBag.MdPageColumns = GalleryConstants.ColumnsFormMd;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
@using NuGetGallery
2+
@model DeleteOrganizationViewModel
3+
@{
4+
ViewBag.Title = "Delete Account " + Model.AccountName;
5+
ViewBag.MdPageColumns = GalleryConstants.ColumnsFormMd;
6+
}
7+
8+
<section role="main" class="container main-container page-delete-account">
9+
<div class="form-group">
10+
<div class="form-group">
11+
@ViewHelpers.Breadcrumb(true, @<text><a href="@Url.User(@Model.User)">@Model.AccountName</a></text>, @<text>Delete organization</text>)
12+
13+
@ViewHelpers.AlertDanger(@<text>
14+
<b class="keywords">Once this organization is deleted, it cannot be undone!</b> <br />
15+
Deleting this organization will: <br />
16+
1. Remove the organization as an owner of any child packages.<br />
17+
2. Delete any API keys that are scoped to this organization.<br />
18+
3. Dissociate all previously existent ID prefix reservations with this organization.<br />
19+
4. Remove all of its members from being a part of the organization or owning its packages.<br />
20+
5. Delete any membership requests to join this organization.<br />
21+
</text>)
22+
23+
<div class="form-group">
24+
@Html.Partial("~/Views/Users/_UserPackagesListForDeletedAccount.cshtml", Model)
25+
</div>
26+
<div class="form-group">
27+
@{
28+
var membersCount = Model.Members.Count();
29+
var membersPlural = membersCount == 1 ? "" : "s";
30+
}
31+
<p>
32+
This organization has @membersCount member@(membersPlural).
33+
</p>
34+
@if (Model.Members.Any())
35+
{
36+
@Html.Partial("_OrganizationMembersListForDeletedAccount", Model.Members)
37+
}
38+
</div>
39+
</div>
40+
<div class="form-group danger-zone">
41+
@using (Html.BeginForm("Delete", "Organizations", FormMethod.Post, new { id = "delete-form" }))
42+
{
43+
@Html.Partial("_DeleteUserAccountForm", new DeleteAccountAsAdminViewModel(Model))
44+
}
45+
</div>
46+
</div>
47+
</section>
48+
49+
@section BottomScripts {
50+
<script type="text/javascript">
51+
$(function () {
52+
$('#delete-form').submit(function (e) {
53+
if (!confirm('Are you sure you want to continue to delete this organization?')) {
54+
e.preventDefault();
55+
}
56+
});
57+
});
58+
</script>
59+
}

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
<section role="main" class="container main-container page-delete-account">
99
<div class="form-group">
1010
<div class="form-group">
11-
@ViewHelpers.Breadcrumb(true, @<text><a href="@Url.User(@Model.User)">@Model.AccountName</a></text>, @<text>Delete account</text>)
11+
@ViewHelpers.Breadcrumb(true, @<text><a href="@Url.User(@Model.User)">@Model.AccountName</a></text>, @<text>Delete user</text>)
1212

1313
@ViewHelpers.AlertDanger(@<text>
14-
<b class="keywords">Once the account is deleted, it cannot be undone!</b> <br />
15-
Deleting the account will: <br />
16-
1. Revoke associated API key(s). <br />
17-
2. Remove the account as an owner for any child packages.<br />
18-
3. Dissociate all previously existent ID prefix reservations with this account.<br />
19-
4. Remove the account as a member of any organizations.<br />
14+
<b class="keywords">Once this user is deleted, it cannot be undone!</b> <br />
15+
Deleting this user will: <br />
16+
1. Revoke any associated API key(s). <br />
17+
2. Remove the user as an owner for any child packages.<br />
18+
3. Dissociate all previously existent ID prefix reservations with this user.<br />
19+
4. Remove the user as a member of any organizations.<br />
2020
</text>)
2121

2222
<div class="form-group">
@@ -26,15 +26,20 @@
2626
@Html.Partial("_UserOrganizationsListForDeletedAccount", Model.Organizations)
2727
</div>
2828
</div>
29-
@Html.Partial("_DeleteUserAccountForm", new DeleteAccountAsAdminViewModel(Model))
29+
<div class="form-group danger-zone">
30+
@using (Html.BeginForm("Delete", "Users", FormMethod.Post, new { id = "delete-form" }))
31+
{
32+
@Html.Partial("_DeleteUserAccountForm", new DeleteAccountAsAdminViewModel(Model))
33+
}
34+
</div>
3035
</div>
3136
</section>
3237

3338
@section BottomScripts {
3439
<script type="text/javascript">
3540
$(function () {
3641
$('#delete-form').submit(function (e) {
37-
if (!confirm('Are you sure you want to continue to delete this account?')) {
42+
if (!confirm('Are you sure you want to continue to delete this user?')) {
3843
e.preventDefault();
3944
}
4045
});
Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,34 @@
11
@model DeleteAccountAsAdminViewModel
22

3-
<div class="form-group danger-zone">
4-
@using (Html.BeginForm("Delete", "Users", FormMethod.Post, new { id = "delete-form" }))
5-
{
6-
@Html.HttpMethodOverride(HttpVerbs.Delete)
7-
@Html.AntiForgeryToken()
8-
@Html.ValidationSummary(true)
9-
<div>
10-
<div class="form-group">
11-
@Html.LabelFor(m => m.Signature)
12-
@Html.EditorFor(m => m.Signature)
13-
@Html.ValidationMessageFor(m => m.Signature)
14-
</div>
3+
@Html.HttpMethodOverride(HttpVerbs.Delete)
4+
@Html.AntiForgeryToken()
5+
@Html.ValidationSummary(true)
6+
<div>
7+
<div class="form-group">
8+
@Html.LabelFor(m => m.Signature)
9+
@Html.EditorFor(m => m.Signature)
10+
@Html.ValidationMessageFor(m => m.Signature)
11+
</div>
1512

16-
@if (Model.HasPackagesThatWillBeOrphaned)
17-
{
18-
<div class="form-group">
19-
@Html.EditorFor(m => m.ShouldUnlist)
20-
<label class="checkbox-inline">
21-
<b>Unlist the packages without any owner.</b>
22-
</label>
23-
<p>
24-
One or more packages do not have co-owners. If you choose to proceed without fixing this issue, these packages will be orphaned and a warning message will be displayed under owners on the package page.
25-
</p>
26-
</div>
27-
}
13+
@if (Model.HasPackagesThatWillBeOrphaned)
14+
{
15+
<div class="form-group">
16+
@Html.EditorFor(m => m.ShouldUnlist)
17+
<label class="checkbox-inline">
18+
<b>Unlist the packages without any owner.</b>
19+
</label>
20+
<p>
21+
One or more packages do not have co-owners. If you choose to proceed without fixing this issue, these packages will be orphaned and a warning message will be displayed under owners on the package page.
22+
</p>
2823
</div>
29-
<hr />
30-
<p>
31-
This action <strong>CANNOT</strong> be undone. You will not be able to regain access to the account <b>@Model.AccountName</b> or any of its packages.
32-
</p>
33-
<hr />
34-
@Html.HiddenFor(m => m.Signature)
35-
@Html.HiddenFor(m => m.ShouldUnlist)
36-
@Html.HiddenFor(m => m.AccountName)
37-
<input id="btn-delete" type="submit" class="btn btn-danger form-control" value="I understand the consequences, delete this account" title="I understand the consequences, delete this account." />
3824
}
39-
</div>
25+
</div>
26+
<hr />
27+
<p>
28+
This action <strong>CANNOT</strong> be undone. You will not be able to regain access to the account <b>@Model.AccountName</b> or any of its packages.
29+
</p>
30+
<hr />
31+
@Html.HiddenFor(m => m.Signature)
32+
@Html.HiddenFor(m => m.ShouldUnlist)
33+
@Html.HiddenFor(m => m.AccountName)
34+
<input id="btn-delete" type="submit" class="btn btn-danger form-control" value="I understand the consequences, delete this account" title="I understand the consequences, delete this account." />

src/NuGetGallery/Controllers/AccountsController.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Web.Mvc;
1010
using NuGet.Services.Entities;
1111
using NuGet.Services.Messaging.Email;
12+
using NuGetGallery.Areas.Admin.ViewModels;
1213
using NuGetGallery.Authentication;
1314
using NuGetGallery.Filters;
1415
using NuGetGallery.Helpers;
@@ -48,6 +49,8 @@ public class ViewMessages
4849

4950
public IMessageServiceConfiguration MessageServiceConfiguration { get; }
5051

52+
public IDeleteAccountService DeleteAccountService { get; }
53+
5154
public AccountsController(
5255
AuthenticationService authenticationService,
5356
IPackageService packageService,
@@ -57,7 +60,8 @@ public AccountsController(
5760
ISecurityPolicyService securityPolicyService,
5861
ICertificateService certificateService,
5962
IContentObjectService contentObjectService,
60-
IMessageServiceConfiguration messageServiceConfiguration)
63+
IMessageServiceConfiguration messageServiceConfiguration,
64+
IDeleteAccountService deleteAccountService)
6165
{
6266
AuthenticationService = authenticationService ?? throw new ArgumentNullException(nameof(authenticationService));
6367
PackageService = packageService ?? throw new ArgumentNullException(nameof(packageService));
@@ -68,6 +72,7 @@ public AccountsController(
6872
CertificateService = certificateService ?? throw new ArgumentNullException(nameof(certificateService));
6973
ContentObjectService = contentObjectService ?? throw new ArgumentNullException(nameof(contentObjectService));
7074
MessageServiceConfiguration = messageServiceConfiguration ?? throw new ArgumentNullException(nameof(messageServiceConfiguration));
75+
DeleteAccountService = deleteAccountService ?? throw new ArgumentNullException(nameof(deleteAccountService));
7176
}
7277

7378
public abstract string AccountAction { get; }
@@ -310,6 +315,48 @@ public virtual ActionResult DeleteRequest(string accountName = null)
310315
return View("DeleteAccount", GetDeleteAccountViewModel(accountToDelete));
311316
}
312317

318+
[HttpGet]
319+
[UIAuthorize(Roles = "Admins")]
320+
public virtual ActionResult Delete(string accountName)
321+
{
322+
var accountToDelete = UserService.FindByUsername(accountName) as TUser;
323+
if (accountToDelete == null || accountToDelete.IsDeleted)
324+
{
325+
return HttpNotFound();
326+
}
327+
328+
return View(GetDeleteAccountViewName(), GetDeleteAccountViewModel(accountToDelete));
329+
}
330+
331+
[HttpDelete]
332+
[UIAuthorize(Roles = "Admins")]
333+
[RequiresAccountConfirmation("Delete account")]
334+
[ValidateAntiForgeryToken]
335+
public virtual async Task<ActionResult> Delete(DeleteAccountAsAdminViewModel model)
336+
{
337+
var accountToDelete = UserService.FindByUsername(model.AccountName) as TUser;
338+
if (accountToDelete == null || accountToDelete.IsDeleted)
339+
{
340+
return View("DeleteAccountStatus", new DeleteAccountStatus()
341+
{
342+
AccountName = model.AccountName,
343+
Description = $"Account {model.AccountName} not found.",
344+
Success = false
345+
});
346+
}
347+
else
348+
{
349+
var admin = GetCurrentUser();
350+
var status = await DeleteAccountService.DeleteAccountAsync(
351+
userToBeDeleted: accountToDelete,
352+
userToExecuteTheDelete: admin,
353+
orphanPackagePolicy: model.ShouldUnlist ? AccountDeletionOrphanPackagePolicy.UnlistOrphans : AccountDeletionOrphanPackagePolicy.KeepOrphans);
354+
return View("DeleteAccountStatus", status);
355+
}
356+
}
357+
358+
protected abstract string GetDeleteAccountViewName();
359+
313360
protected abstract DeleteAccountViewModel<TUser> GetDeleteAccountViewModel(TUser account);
314361

315362
public abstract Task<ActionResult> RequestAccountDeletion(string accountName = null);

src/NuGetGallery/Controllers/OrganizationsController.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ namespace NuGetGallery
2020
public class OrganizationsController
2121
: AccountsController<Organization, OrganizationAccountViewModel>
2222
{
23-
public IDeleteAccountService DeleteAccountService { get; }
24-
2523
public OrganizationsController(
2624
AuthenticationService authService,
2725
IMessageService messageService,
@@ -42,9 +40,9 @@ public OrganizationsController(
4240
securityPolicyService,
4341
certificateService,
4442
contentObjectService,
45-
messageServiceConfiguration)
43+
messageServiceConfiguration,
44+
deleteAccountService)
4645
{
47-
DeleteAccountService = deleteAccountService;
4846
}
4947

5048
public override string AccountAction => nameof(ManageOrganization);
@@ -326,6 +324,8 @@ public async Task<JsonResult> DeleteMember(string accountName, string memberName
326324
}
327325
}
328326

327+
protected override string GetDeleteAccountViewName() => "DeleteOrganizationAccount";
328+
329329
protected override DeleteAccountViewModel<Organization> GetDeleteAccountViewModel(Organization account)
330330
{
331331
return GetDeleteOrganizationViewModel(account);

src/NuGetGallery/Controllers/UsersController.cs

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public partial class UsersController
2929
private readonly IPackageOwnerRequestService _packageOwnerRequestService;
3030
private readonly IAppConfiguration _config;
3131
private readonly ICredentialBuilder _credentialBuilder;
32-
private readonly IDeleteAccountService _deleteAccountService;
3332
private readonly ISupportRequestService _supportRequestService;
3433

3534
public UsersController(
@@ -56,12 +55,12 @@ public UsersController(
5655
securityPolicyService,
5756
certificateService,
5857
contentObjectService,
59-
messageServiceConfiguration)
58+
messageServiceConfiguration,
59+
deleteAccountService)
6060
{
6161
_packageOwnerRequestService = packageOwnerRequestService ?? throw new ArgumentNullException(nameof(packageOwnerRequestService));
6262
_config = config ?? throw new ArgumentNullException(nameof(config));
6363
_credentialBuilder = credentialBuilder ?? throw new ArgumentNullException(nameof(credentialBuilder));
64-
_deleteAccountService = deleteAccountService ?? throw new ArgumentNullException(nameof(deleteAccountService));
6564
_supportRequestService = supportRequestService ?? throw new ArgumentNullException(nameof(supportRequestService));
6665
}
6766

@@ -105,6 +104,8 @@ protected override User GetAccount(string accountName)
105104
return null;
106105
}
107106

107+
protected override string GetDeleteAccountViewName() => "DeleteUserAccount";
108+
108109
protected override DeleteAccountViewModel<User> GetDeleteAccountViewModel(User account)
109110
{
110111
return new DeleteUserViewModel(account, GetCurrentUser(), PackageService, _supportRequestService);
@@ -331,7 +332,7 @@ public override async Task<ActionResult> RequestAccountDeletion(string accountNa
331332
if (!user.Confirmed)
332333
{
333334
// Unconfirmed users can be deleted immediately without creating a support request.
334-
DeleteUserAccountStatus accountDeleteStatus = await _deleteAccountService.DeleteAccountAsync(userToBeDeleted: user,
335+
DeleteAccountStatus accountDeleteStatus = await DeleteAccountService.DeleteAccountAsync(userToBeDeleted: user,
335336
userToExecuteTheDelete: user,
336337
orphanPackagePolicy: AccountDeletionOrphanPackagePolicy.UnlistOrphans);
337338
if (!accountDeleteStatus.Success)
@@ -357,46 +358,6 @@ public override async Task<ActionResult> RequestAccountDeletion(string accountNa
357358
return RedirectToAction(nameof(DeleteRequest));
358359
}
359360

360-
[HttpGet]
361-
[UIAuthorize(Roles = "Admins")]
362-
public virtual ActionResult Delete(string accountName)
363-
{
364-
var user = UserService.FindByUsername(accountName);
365-
if (user == null || user.IsDeleted || (user is Organization))
366-
{
367-
return HttpNotFound();
368-
}
369-
370-
return View("DeleteUserAccount", GetDeleteAccountViewModel(user));
371-
}
372-
373-
[HttpDelete]
374-
[UIAuthorize(Roles = "Admins")]
375-
[RequiresAccountConfirmation("Delete account")]
376-
[ValidateAntiForgeryToken]
377-
public virtual async Task<ActionResult> Delete(DeleteAccountAsAdminViewModel model)
378-
{
379-
var user = UserService.FindByUsername(model.AccountName);
380-
if (user == null || user.IsDeleted)
381-
{
382-
return View("DeleteUserAccountStatus", new DeleteUserAccountStatus()
383-
{
384-
AccountName = model.AccountName,
385-
Description = $"Account {model.AccountName} not found.",
386-
Success = false
387-
});
388-
}
389-
else
390-
{
391-
var admin = GetCurrentUser();
392-
var status = await _deleteAccountService.DeleteAccountAsync(
393-
userToBeDeleted: user,
394-
userToExecuteTheDelete: admin,
395-
orphanPackagePolicy: model.ShouldUnlist ? AccountDeletionOrphanPackagePolicy.UnlistOrphans : AccountDeletionOrphanPackagePolicy.KeepOrphans);
396-
return View("DeleteUserAccountStatus", status);
397-
}
398-
}
399-
400361
[HttpGet]
401362
[UIAuthorize]
402363
public virtual ActionResult ApiKeys()

0 commit comments

Comments
 (0)