Skip to content

Commit d87a5ba

Browse files
authored
Add new admin panel for limiting operations allowed by some users (#8949)
See NuGet/Engineering#4213 for more information
1 parent 8b7df7d commit d87a5ba

47 files changed

Lines changed: 1290 additions & 125 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/NuGet.Services.Entities/User.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ public bool Confirmed
120120

121121
public int FailedLoginCount { get; set; }
122122

123+
public UserStatus UserStatusKey { get; set; }
124+
125+
public bool IsLocked => UserStatusKey == UserStatus.Locked;
126+
123127
public string LastSavedEmailAddress
124128
{
125129
get
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace NuGet.Services.Entities
5+
{
6+
public enum UserStatus
7+
{
8+
/// <summary>
9+
/// The user is active and can perform all user operations. This does not consider additional requirements based on
10+
/// <see cref="User.Confirmed"/>, <see cref="User.FailedLoginCount"/>, or <see cref="User.IsDeleted"/>.
11+
/// </summary>
12+
Unlocked = 0,
13+
14+
// enum value 1 is intentionally unused to allow future use for a "Deleted" status, to align with the
15+
// PackageStatus enum.
16+
17+
/// <summary>
18+
/// The user is locked and is restricted from performing some operations.
19+
/// </summary>
20+
Locked = 2,
21+
}
22+
}

src/NuGetGallery.Core/Auditing/AuditedPackageAction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public enum AuditedPackageAction
1919
SymbolsCreate,
2020
SymbolsDelete,
2121
Deprecate,
22-
Undeprecate
22+
Undeprecate,
2323
}
2424
}

src/NuGetGallery.Core/Auditing/AuditedPackageRegistrationAction.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ public enum AuditedPackageRegistrationAction
1212
SetRequiredSigner,
1313
AddOwnershipRequest,
1414
DeleteOwnershipRequest,
15+
Lock,
16+
Unlock,
1517
}
1618
}

src/NuGetGallery.Core/Auditing/AuditedUserAction.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@ public enum AuditedUserAction
2626
EnabledMultiFactorAuthentication,
2727
DisabledMultiFactorAuthentication,
2828
ExternalLoginAttempt,
29+
Lock,
30+
Unlock,
2931
}
3032
}

src/NuGetGallery.Services/Authentication/ApiScopeEvaluationResult.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class ApiScopeEvaluationResult
2828
public User Owner { get; }
2929

3030
public bool IsOwnerConfirmed => Owner != null && Owner.Confirmed;
31+
public bool IsOwnerLocked => Owner != null && Owner.IsLocked;
3132

3233
public ApiScopeEvaluationResult(User owner, PermissionsCheckResult permissionsCheckResult, bool scopesAreValid)
3334
{
@@ -42,7 +43,10 @@ public ApiScopeEvaluationResult(User owner, PermissionsCheckResult permissionsCh
4243
/// </summary>
4344
public bool IsSuccessful()
4445
{
45-
return ScopesAreValid && PermissionsCheckResult == PermissionsCheckResult.Allowed && IsOwnerConfirmed;
46+
return ScopesAreValid
47+
&& PermissionsCheckResult == PermissionsCheckResult.Allowed
48+
&& IsOwnerConfirmed
49+
&& !IsOwnerLocked;
4650
}
4751
}
4852
}

src/NuGetGallery.Services/ServicesStrings.Designer.cs

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/NuGetGallery.Services/ServicesStrings.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,4 +1130,13 @@ The {1} Team</value>
11301130
<data name="NuGetPackageDuplicateDependencyGroup" xml:space="preserve">
11311131
<value>A nuget package may not contain multiple dependency groups with the same target framework.</value>
11321132
</data>
1133+
<data name="UserAccountIsLocked" xml:space="preserve">
1134+
<value>Your user account is locked. Please contact [email protected].</value>
1135+
</data>
1136+
<data name="SpecificAccountIsLocked" xml:space="preserve">
1137+
<value>Account '{0}' is locked. Please contact [email protected].</value>
1138+
</data>
1139+
<data name="TransformAccount_AccountIsLocked" xml:space="preserve">
1140+
<value>Account '{0}' is locked and cannot be transformed to an organization. Please contact [email protected].</value>
1141+
</data>
11331142
</root>

src/NuGetGallery.Services/UserManagement/UserService.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,11 @@ public async Task ChangeEmailAddress(User user, string newEmailAddress)
400400
throw new EntityException(ServicesStrings.EmailAddressBeingUsed, newEmailAddress);
401401
}
402402

403+
if (user.IsLocked)
404+
{
405+
throw new EntityException(String.Format(CultureInfo.CurrentCulture, ServicesStrings.SpecificAccountIsLocked, user.Username));
406+
}
407+
403408
await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.ChangeEmail, newEmailAddress));
404409

405410
user.UpdateUnconfirmedEmailAddress(newEmailAddress, Crypto.GenerateToken);
@@ -499,6 +504,11 @@ public bool CanTransformUserToOrganization(User accountToTransform, out string e
499504
errorReason = String.Format(CultureInfo.CurrentCulture,
500505
ServicesStrings.TransformAccount_AccountNotConfirmed, accountToTransform.Username);
501506
}
507+
else if (accountToTransform.IsLocked)
508+
{
509+
errorReason = String.Format(CultureInfo.CurrentCulture,
510+
ServicesStrings.TransformAccount_AccountIsLocked, accountToTransform.Username);
511+
}
502512
else if (accountToTransform is Organization)
503513
{
504514
errorReason = String.Format(CultureInfo.CurrentCulture,
@@ -574,6 +584,11 @@ public async Task<Organization> AddOrganizationAsync(string username, string ema
574584
}
575585
}
576586

587+
if (adminUser.IsLocked)
588+
{
589+
throw new EntityException(ServicesStrings.UserAccountIsLocked);
590+
}
591+
577592
var organization = new Organization(username)
578593
{
579594
EmailAllowed = true,

src/NuGetGallery/Areas/Admin/Controllers/LockPackageController.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,29 @@
99
using System.Web.Mvc;
1010
using NuGet.Services.Entities;
1111
using NuGetGallery.Areas.Admin.ViewModels;
12+
using NuGetGallery.Auditing;
1213

1314
namespace NuGetGallery.Areas.Admin.Controllers
1415
{
1516
public class LockPackageController : AdminControllerBase
1617
{
17-
private IEntityRepository<PackageRegistration> _packageRegistrationRepository;
18+
private readonly IEntityRepository<PackageRegistration> _packageRegistrationRepository;
19+
private readonly IAuditingService _auditingService;
1820

19-
public LockPackageController(IEntityRepository<PackageRegistration> packageRegistrationRepository)
21+
public LockPackageController(
22+
IEntityRepository<PackageRegistration> packageRegistrationRepository,
23+
IAuditingService auditingService)
2024
{
2125
_packageRegistrationRepository = packageRegistrationRepository ?? throw new ArgumentNullException(nameof(packageRegistrationRepository));
26+
_auditingService = auditingService ?? throw new ArgumentNullException(nameof(auditingService));
2227
}
2328

2429
[HttpGet]
2530
public virtual ActionResult Index()
2631
{
2732
var model = new LockPackageViewModel();
2833

29-
return View(model);
34+
return View("LockIndex", model);
3035
}
3136

3237
[HttpGet]
@@ -35,25 +40,30 @@ public virtual ActionResult Search(string query)
3540
var lines = Helpers.ParseQueryToLines(query);
3641
var packageRegistrations = GetPackageRegistrationsForIds(lines);
3742

38-
return View(nameof(Index), new LockPackageViewModel()
43+
return View("LockIndex", new LockPackageViewModel
3944
{
4045
Query = query,
41-
PackageLockStates = packageRegistrations.Select(x => new PackageLockState() { Id = x.Id, IsLocked = x.IsLocked }).ToList()
46+
LockStates = packageRegistrations
47+
.Select(x => new LockState { Identifier = x.Id, IsLocked = x.IsLocked })
48+
.ToList()
4249
});
4350
}
4451

4552
[HttpPost]
4653
[ValidateAntiForgeryToken]
47-
public async Task<ActionResult> Update(LockPackageViewModel lockPackageViewModel)
54+
public async Task<ActionResult> Update(LockPackageViewModel viewModel)
4855
{
49-
int counter = 0;
56+
var counter = 0;
57+
viewModel = viewModel ?? new LockPackageViewModel();
5058

51-
if (lockPackageViewModel != null && lockPackageViewModel.PackageLockStates != null)
59+
if (viewModel.LockStates != null)
5260
{
53-
var packageIdsFromRequest = lockPackageViewModel.PackageLockStates.Select(x => x.Id).ToList();
61+
var packageIdsFromRequest = viewModel.LockStates.Select(x => x.Identifier).ToList();
5462
var packageRegistrationsFromDb = GetPackageRegistrationsForIds(packageIdsFromRequest);
5563

56-
var packageStatesFromRequestDictionary = lockPackageViewModel.PackageLockStates.ToDictionary(x => x.Id);
64+
var packageStatesFromRequestDictionary = viewModel
65+
.LockStates
66+
.ToDictionary(x => x.Identifier, StringComparer.OrdinalIgnoreCase);
5767

5868
foreach (var packageRegistration in packageRegistrationsFromDb)
5969
{
@@ -63,6 +73,10 @@ public async Task<ActionResult> Update(LockPackageViewModel lockPackageViewModel
6373
{
6474
packageRegistration.IsLocked = packageStateRequest.IsLocked;
6575
counter++;
76+
await _auditingService.SaveAuditRecordAsync(new PackageRegistrationAuditRecord(
77+
packageRegistration,
78+
packageStateRequest.IsLocked ? AuditedPackageRegistrationAction.Lock : AuditedPackageRegistrationAction.Unlock,
79+
owner: null));
6680
}
6781
}
6882
}
@@ -75,7 +89,7 @@ public async Task<ActionResult> Update(LockPackageViewModel lockPackageViewModel
7589

7690
TempData["Message"] = string.Format(CultureInfo.InvariantCulture, $"Lock state was updated for {counter} packages.");
7791

78-
return View(nameof(Index), lockPackageViewModel);
92+
return View("LockIndex", viewModel);
7993
}
8094

8195
private IList<PackageRegistration> GetPackageRegistrationsForIds(IReadOnlyList<string> ids)

0 commit comments

Comments
 (0)