+
+
+
+ @using (Html.BeginForm("Enroll", "SecurityPolicy", new { area = "Admin" }, FormMethod.Post, new { id = "delete-form" }))
+ {
+
+
+
+
+ The following users were not found:
+
+
+
+
+
+
+
+
Username
+ @foreach (var policyGroup in Model.PolicyGroups)
+ {
+
@policyGroup
+ }
+
+
+
+
+
+ @foreach (var policyGroup in Model.PolicyGroups)
+ {
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+@section BottomScripts {
+
+
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj
index 11698c8ae6..d08969f140 100644
--- a/src/NuGetGallery/NuGetGallery.csproj
+++ b/src/NuGetGallery/NuGetGallery.csproj
@@ -643,6 +643,7 @@
+ Url_Edit.ascx
@@ -678,6 +679,8 @@
+
+
@@ -749,6 +752,8 @@
201705032101231_SecurityPoliciesFix.cs
+
+
@@ -1714,6 +1719,7 @@
+
diff --git a/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs b/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
new file mode 100644
index 0000000000..20c7fe0dc4
--- /dev/null
+++ b/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
@@ -0,0 +1,77 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace NuGetGallery.Security
+{
+ public static class UserSecurityPolicyExtensions
+ {
+ ///
+ /// Determine whether two security policies are equivalent.
+ ///
+ public static bool Matches(this UserSecurityPolicy first, UserSecurityPolicy second)
+ {
+ return first.Name.Equals(second.Name, StringComparison.OrdinalIgnoreCase) &&
+ (
+ (string.IsNullOrEmpty(first.Value) && string.IsNullOrEmpty(second.Value)) ||
+ (first.Value.Equals(second.Value, StringComparison.OrdinalIgnoreCase))
+ );
+ }
+
+ ///
+ /// Check whether a user has the security policies required for a policy group.
+ ///
+ /// True if enrolled (has all policies), false otherwise.
+ public static bool IsEnrolled(this User user, UserSecurityPolicyGroup policyGroup)
+ {
+ return !user.FindPolicies(policyGroup).Any(p => p == null);
+ }
+
+ ///
+ /// Ensure user is enrolled in the security policy group.
+ ///
+ /// User to enroll.
+ /// User security policy group to enroll in.
+ public static void EnsureEnrolled(this User user, UserSecurityPolicyGroup policyGroup)
+ {
+ // Add policies, if not already enrolled in all group policies.
+ if (!user.IsEnrolled(policyGroup))
+ {
+ foreach (var policy in policyGroup.Policies)
+ {
+ user.SecurityPolicies.Add(policy);
+ }
+ policyGroup.OnEnroll?.Invoke(user);
+ }
+ }
+
+ ///
+ /// Ensure user is unenrolled from the security policy group.
+ ///
+ /// User to unenroll.
+ /// User security policy group to unenroll from.
+ public static void EnsureUnenrolled(this User user, UserSecurityPolicyGroup policyGroup)
+ {
+ // Remove policies, only if enrolled in all group policies.
+ var matches = user.FindPolicies(policyGroup);
+ if (!matches.Any(p => p == null))
+ {
+ foreach (var policy in matches)
+ {
+ user.SecurityPolicies.Remove(policy);
+ }
+ }
+ }
+
+ ///
+ /// Find user security policies which are part of a security policy group.
+ ///
+ private static IEnumerable FindPolicies(this User user, UserSecurityPolicyGroup policyGroup)
+ {
+ return policyGroup.Policies.Select(gp => user.SecurityPolicies.FirstOrDefault(up => up.Matches(gp)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/Security/UserSecurityPolicyGroup.cs b/src/NuGetGallery/Security/UserSecurityPolicyGroup.cs
new file mode 100644
index 0000000000..4c607513d1
--- /dev/null
+++ b/src/NuGetGallery/Security/UserSecurityPolicyGroup.cs
@@ -0,0 +1,81 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NuGetGallery.Authentication;
+
+namespace NuGetGallery.Security
+{
+ ///
+ /// Grouping of one or more user security policies for enrollment.
+ ///
+ public class UserSecurityPolicyGroup
+ {
+ public const string SecurePush = "SecurePush";
+
+ ///
+ /// Policy group name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Required policies.
+ ///
+ public IEnumerable Policies { get; set; }
+
+ ///
+ /// Action to take on user enrollment.
+ ///
+ public Action OnEnroll { get; set; }
+
+ ///
+ /// Get supported user security policy groups.
+ ///
+ private static List _instances;
+ public static List Instances
+ {
+ get
+ {
+ if (_instances == null)
+ {
+ _instances = CreateUserPolicyGroups().ToList();
+ }
+ return _instances;
+ }
+ }
+
+ private static IEnumerable CreateUserPolicyGroups()
+ {
+ yield return new UserSecurityPolicyGroup()
+ {
+ Name = UserSecurityPolicyGroup.SecurePush,
+ Policies = new []
+ {
+ new UserSecurityPolicy(RequirePackageVerifyScopePolicy.PolicyName),
+ new UserSecurityPolicy(RequireMinClientVersionForPushPolicy.PolicyName) { Value = "{\"v\":\"4.1.0\"}" }
+ },
+ OnEnroll = OnEnroll_SecurePush
+ };
+ }
+
+ private static void OnEnroll_SecurePush(User user)
+ {
+ var pushKeys = user.Credentials.Where(c =>
+ c.Type.StartsWith(CredentialTypes.ApiKey.Prefix) && !c.HasExpired &&
+ (
+ c.Scopes.Count == 0 ||
+ c.Scopes.Any(s =>
+ s.AllowedAction.Equals(NuGetScopes.PackagePush, StringComparison.OrdinalIgnoreCase) ||
+ s.AllowedAction.Equals(NuGetScopes.PackagePushVersion, StringComparison.OrdinalIgnoreCase)
+ ))
+ );
+
+ foreach (var key in pushKeys)
+ {
+ key.Expires = DateTime.Now.AddDays(7);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj
index 100854b412..142bed6cc3 100644
--- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj
+++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj
@@ -411,6 +411,8 @@
+
+
diff --git a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
new file mode 100644
index 0000000000..f81c59bd44
--- /dev/null
+++ b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
@@ -0,0 +1,134 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+using Newtonsoft.Json;
+
+namespace NuGetGallery.Security
+{
+ public class UserSecurityPolicyExtensionsFacts
+ {
+ [Theory]
+ [InlineData("", "")]
+ [InlineData(null, "")]
+ [InlineData(null, null)]
+ public void MatchesReturnsTrue(string value1, string value2)
+ {
+ // Arrange.
+ var policy1 = new UserSecurityPolicy("A") { Value = value1 };
+ var policy2 = new UserSecurityPolicy("A") { Value = value2 };
+
+ // Act & Assert.
+ Assert.True(policy1.Matches(policy2));
+ }
+
+ [Fact]
+ public void MatchesReturnsFalseIfNameDiffers()
+ {
+ // Arrange.
+ var policy1 = new UserSecurityPolicy("A");
+ var policy2 = new UserSecurityPolicy("B");
+
+ // Act & Assert.
+ Assert.False(policy1.Matches(policy2));
+ }
+
+ [Fact]
+ public void MatchesReturnsFalseIfValueDiffers()
+ {
+ // Arrange.
+ var policy1 = new UserSecurityPolicy("A") { Value = "B" };
+ var policy2 = new UserSecurityPolicy("A") { Value = "C" };
+
+ // Act & Assert.
+ Assert.False(policy1.Matches(policy2));
+ }
+
+ [Theory]
+ [InlineData("[[\"A\",\"\"]]", "[[\"A\",null]]")]
+ [InlineData("[[\"A\",\"B\"],[\"E\",\"\"]]", "[[\"E\",\"\"],[\"A\",\"B\"],[\"C\",\"D\"]]")]
+ public void IsEnrolledReturnsTrue(string groupPolicies, string userPolicies)
+ {
+ // Arrange.
+ var group = new UserSecurityPolicyGroup()
+ {
+ Policies = LoadPolicies(groupPolicies)
+ };
+ var user = new User();
+ LoadPolicies(userPolicies).ToList().ForEach(p => user.SecurityPolicies.Add(p));
+
+ // Act & Assert.
+ Assert.True(user.IsEnrolled(group));
+ }
+
+ [Theory]
+ [InlineData("[[\"A\",\"B\"],[\"E\",null]]", "[]")]
+ [InlineData("[[\"A\",\"B\"],[\"E\",null]]", "[[\"A\",\"B\"],[\"C\",\"D\"]]")]
+ public void IsEnrolledReturnsFalse(string groupPolicies, string userPolicies)
+ {
+ // Arrange.
+ var group = new UserSecurityPolicyGroup()
+ {
+ Policies = LoadPolicies(groupPolicies)
+ };
+ var user = new User();
+ LoadPolicies(userPolicies).ToList().ForEach(p => user.SecurityPolicies.Add(p));
+
+ // Act & Assert.
+ Assert.False(user.IsEnrolled(group));
+ }
+
+ [Theory]
+ [InlineData("[[\"A\",\"\"]]")]
+ [InlineData("[[\"A\",\"B\"],[\"E\",\"\"]]")]
+ public void EnsureEnrolledAddsPolicies(string groupPolicies)
+ {
+ // Arrange.
+ var group = new UserSecurityPolicyGroup()
+ {
+ Policies = LoadPolicies(groupPolicies)
+ };
+ var user = new User();
+
+ // Act.
+ user.EnsureEnrolled(group);
+
+ // Assert.
+ Assert.Equal(group.Policies.Count(), user.SecurityPolicies.Count());
+ }
+
+ [Theory]
+ [InlineData("[[\"A\",\"\"]]", "[[\"A\",null]]", 0)]
+ [InlineData("[[\"A\",\"B\"],[\"E\",\"\"]]", "[[\"E\",\"\"],[\"A\",\"B\"],[\"C\",\"D\"]]", 1)]
+ public void EnsureUnEnrolledAddsPolicies(string groupPolicies, string userPolicies, int expectedCount)
+ {
+ // Arrange.
+ var group = new UserSecurityPolicyGroup()
+ {
+ Policies = LoadPolicies(groupPolicies)
+ };
+ var user = new User();
+ LoadPolicies(userPolicies).ToList().ForEach(p => user.SecurityPolicies.Add(p));
+
+ // Act.
+ user.EnsureUnenrolled(group);
+
+ // Assert.
+ Assert.Equal(expectedCount, user.SecurityPolicies.Count());
+ }
+
+ private IEnumerable LoadPolicies(string policiesString)
+ {
+ var policies = (string[][])JsonConvert.DeserializeObject(policiesString);
+ if (policies != null)
+ {
+ foreach (var p in policies)
+ {
+ yield return new UserSecurityPolicy(p[0]) { Value = p[1] };
+ }
+ }
+ }
+ }
+}
diff --git a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
new file mode 100644
index 0000000000..72d70c517b
--- /dev/null
+++ b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
@@ -0,0 +1,68 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using NuGet.Packaging;
+using Xunit;
+
+namespace NuGetGallery.Security
+{
+ public class UserSecurityPolicyGroupFacts
+ {
+ [Theory]
+ [InlineData("")]
+ [InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]")]
+ [InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]")]
+ [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"theId\"}]")]
+ public void SecurePush_OnEnrollExpiresPushApiKeys(string scopes)
+ {
+ // Arrange.
+ var group = UserSecurityPolicyGroup.Instances.First(
+ g => g.Name.Equals(UserSecurityPolicyGroup.SecurePush, StringComparison.OrdinalIgnoreCase));
+
+ var credential = new Credential(CredentialTypes.ApiKey.V2, string.Empty, TimeSpan.FromDays(10));
+ if (!string.IsNullOrWhiteSpace(scopes))
+ {
+ credential.Scopes.AddRange(JsonConvert.DeserializeObject>(scopes));
+ }
+ var user = new User();
+ user.Credentials.Add(credential);
+
+ // Act.
+ user.EnsureEnrolled(group);
+
+ // Assert.
+ Assert.Equal(2, user.SecurityPolicies.Count());
+ Assert.True(DateTime.UtcNow.AddDays(7) >= user.Credentials.First().Expires);
+ }
+
+ [Theory]
+ [InlineData("password.v3", "")]
+ [InlineData("apikey.v2", "[{\"a\":\"package:unlist\", \"s\":\"theId\"}]")]
+ [InlineData("apikey.verify.v1", "[{\"a\":\"package:verify\", \"s\":\"theId\"}]")]
+ public void SecurePush_OnEnrollDoesNotExpireNonPushCredentials(string type, string scopes)
+ {
+ // Arrange.
+ var group = UserSecurityPolicyGroup.Instances.First(
+ g => g.Name.Equals(UserSecurityPolicyGroup.SecurePush, StringComparison.OrdinalIgnoreCase));
+
+ var credential = new Credential(type, string.Empty, TimeSpan.FromDays(10));
+ if (!string.IsNullOrWhiteSpace(scopes))
+ {
+ credential.Scopes.AddRange(JsonConvert.DeserializeObject>(scopes));
+ }
+ var user = new User();
+ user.Credentials.Add(credential);
+
+ // Act.
+ user.EnsureEnrolled(group);
+
+ // Assert.
+ Assert.Equal(2, user.SecurityPolicies.Count());
+ Assert.False(DateTime.UtcNow.AddDays(7) >= user.Credentials.First().Expires);
+ }
+ }
+}
From 82af4f4d577a3ed35f6eb8d658813f9f8243213c Mon Sep 17 00:00:00 2001
From: Christy Henriksson
Date: Mon, 1 May 2017 17:08:43 -0700
Subject: [PATCH 2/9] Bug fixes
---
.../Entities/EntitiesContext.cs | 1 +
.../Entities/IEntitiesContext.cs | 3 ++-
.../Entities/UserSecurityPolicy.cs | 6 ++++++
.../Controllers/SecurityPolicyController.cs | 19 ++++++++++++-------
.../Security/UserSecurityPolicyExtensions.cs | 7 ++++---
.../UserSecurityPolicyExtensionsFacts.cs | 5 +++--
.../Security/UserSecurityPolicyGroupFacts.cs | 5 +++--
.../TestUtils/FakeEntitiesContext.cs | 12 ++++++++++++
8 files changed, 43 insertions(+), 15 deletions(-)
diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs
index f11172287a..0737e8e8df 100644
--- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs
+++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs
@@ -42,6 +42,7 @@ public EntitiesContext(string connectionString, bool readOnly)
public IDbSet Credentials { get; set; }
public IDbSet Scopes { get; set; }
public IDbSet Users { get; set; }
+ public IDbSet UserSecurityPolicies { get; set; }
IDbSet IEntitiesContext.Set()
{
diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs
index 20c3d645c0..e24dd4089b 100644
--- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs
+++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs
@@ -13,8 +13,9 @@ public interface IEntitiesContext
IDbSet PackageRegistrations { get; set; }
IDbSet Credentials { get; set; }
IDbSet Scopes { get; set; }
-
IDbSet Users { get; set; }
+ IDbSet UserSecurityPolicies { get; set; }
+
Task SaveChangesAsync();
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification="This is to match the EF terminology.")]
IDbSet Set() where T : class;
diff --git a/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs b/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs
index 1ee4721575..649b8372c2 100644
--- a/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs
+++ b/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs
@@ -19,6 +19,12 @@ public UserSecurityPolicy(string name)
Name = name;
}
+ public UserSecurityPolicy(string name, string value)
+ {
+ Name = name;
+ Value = value;
+ }
+
///
/// Policy key.
///
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
index 76fa0a0d94..dea37ee4d9 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using System.Web.Mvc;
using NuGetGallery.Areas.Admin.ViewModels;
using NuGetGallery.Security;
@@ -59,10 +60,10 @@ public virtual ActionResult Search(string query)
[HttpPost]
[ValidateAntiForgeryToken]
- public ActionResult Enroll(SecurityPolicyViewModel viewModel)
+ public async Task Enroll(SecurityPolicyViewModel viewModel)
{
// Parse 'username|policyGroup' into enrollment requests by user.
- var enrollments = viewModel.Enrollments
+ var enrollments = viewModel.Enrollments?
.Select(e => e.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
.GroupBy(e => /*username*/e[0])
.ToDictionary(
@@ -77,20 +78,24 @@ public ActionResult Enroll(SecurityPolicyViewModel viewModel)
{
foreach (var policyGroup in UserSecurityPolicyGroup.Instances)
{
- if (enrollments[user.Username].Contains(policyGroup.Name))
+ if (enrollments != null && enrollments[user.Username].Contains(policyGroup.Name))
{
- user.EnsureEnrolled(policyGroup);
+ user.AddPolicies(policyGroup);
}
else
{
- user.EnsureUnenrolled(policyGroup);
+ var removedPolicies = user.RemovePolicies(policyGroup);
+ foreach (var p in removedPolicies)
+ {
+ EntitiesContext.UserSecurityPolicies.Remove(p);
+ }
}
}
}
- EntitiesContext.SaveChangesAsync();
+ await EntitiesContext.SaveChangesAsync();
- return RedirectToRoute("Admin");
+ return RedirectToAction("Index");
}
private static string[] GetUsernamesFromQuery(string query)
diff --git a/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs b/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
index 20c7fe0dc4..e15768d30d 100644
--- a/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
+++ b/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
@@ -35,14 +35,14 @@ public static bool IsEnrolled(this User user, UserSecurityPolicyGroup policyGrou
///
/// User to enroll.
/// User security policy group to enroll in.
- public static void EnsureEnrolled(this User user, UserSecurityPolicyGroup policyGroup)
+ public static void AddPolicies(this User user, UserSecurityPolicyGroup policyGroup)
{
// Add policies, if not already enrolled in all group policies.
if (!user.IsEnrolled(policyGroup))
{
foreach (var policy in policyGroup.Policies)
{
- user.SecurityPolicies.Add(policy);
+ user.SecurityPolicies.Add(new UserSecurityPolicy(policy.Name, policy.Value));
}
policyGroup.OnEnroll?.Invoke(user);
}
@@ -53,7 +53,7 @@ public static void EnsureEnrolled(this User user, UserSecurityPolicyGroup policy
///
/// User to unenroll.
/// User security policy group to unenroll from.
- public static void EnsureUnenrolled(this User user, UserSecurityPolicyGroup policyGroup)
+ public static IEnumerable RemovePolicies(this User user, UserSecurityPolicyGroup policyGroup)
{
// Remove policies, only if enrolled in all group policies.
var matches = user.FindPolicies(policyGroup);
@@ -62,6 +62,7 @@ public static void EnsureUnenrolled(this User user, UserSecurityPolicyGroup poli
foreach (var policy in matches)
{
user.SecurityPolicies.Remove(policy);
+ yield return policy;
}
}
}
diff --git a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
index f81c59bd44..0be05643be 100644
--- a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
+++ b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
@@ -5,6 +5,7 @@
using System.Linq;
using Xunit;
using Newtonsoft.Json;
+using System.Web.Mvc;
namespace NuGetGallery.Security
{
@@ -93,7 +94,7 @@ public void EnsureEnrolledAddsPolicies(string groupPolicies)
var user = new User();
// Act.
- user.EnsureEnrolled(group);
+ user.AddPolicies(group);
// Assert.
Assert.Equal(group.Policies.Count(), user.SecurityPolicies.Count());
@@ -113,7 +114,7 @@ public void EnsureUnEnrolledAddsPolicies(string groupPolicies, string userPolici
LoadPolicies(userPolicies).ToList().ForEach(p => user.SecurityPolicies.Add(p));
// Act.
- user.EnsureUnenrolled(group);
+ user.RemovePolicies(group);
// Assert.
Assert.Equal(expectedCount, user.SecurityPolicies.Count());
diff --git a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
index 72d70c517b..e0b606a78b 100644
--- a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
+++ b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
@@ -7,6 +7,7 @@
using Newtonsoft.Json;
using NuGet.Packaging;
using Xunit;
+using System.Web.Mvc;
namespace NuGetGallery.Security
{
@@ -32,7 +33,7 @@ public void SecurePush_OnEnrollExpiresPushApiKeys(string scopes)
user.Credentials.Add(credential);
// Act.
- user.EnsureEnrolled(group);
+ user.AddPolicies(group);
// Assert.
Assert.Equal(2, user.SecurityPolicies.Count());
@@ -58,7 +59,7 @@ public void SecurePush_OnEnrollDoesNotExpireNonPushCredentials(string type, stri
user.Credentials.Add(credential);
// Act.
- user.EnsureEnrolled(group);
+ user.AddPolicies(group);
// Assert.
Assert.Equal(2, user.SecurityPolicies.Count());
diff --git a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs
index 969583c167..9afd485f80 100644
--- a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs
+++ b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs
@@ -98,6 +98,18 @@ public IDbSet Users
}
}
+ public IDbSet UserSecurityPolicies
+ {
+ get
+ {
+ return Set();
+ }
+ set
+ {
+ throw new NotSupportedException();
+ }
+ }
+
public Task SaveChangesAsync()
{
_areChangesSaved = true;
From 003161f159ecbcfb8aaf3e6be3e50b8b68e642ba Mon Sep 17 00:00:00 2001
From: Christy Henriksson
Date: Mon, 1 May 2017 17:17:34 -0700
Subject: [PATCH 3/9] Cleanup
---
.../Areas/Admin/Controllers/SecurityPolicyController.cs | 2 +-
.../Security/UserSecurityPolicyExtensionsFacts.cs | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
index dea37ee4d9..ec7b615854 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
@@ -71,7 +71,7 @@ public async Task Enroll(SecurityPolicyViewModel viewModel)
g => g.Select(e => /*policyGroup*/e[1])
);
- // Iterate all users and policies to identity groups for both enrollment and unenrollment.
+ // Iterate all users and groups for enrollment or unenrollment.
var usernames = GetUsernamesFromQuery(viewModel.Query);
var users = FindUsers(usernames);
foreach (var user in users)
diff --git a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
index 0be05643be..57c09230e5 100644
--- a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
+++ b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
@@ -84,7 +84,7 @@ public void IsEnrolledReturnsFalse(string groupPolicies, string userPolicies)
[Theory]
[InlineData("[[\"A\",\"\"]]")]
[InlineData("[[\"A\",\"B\"],[\"E\",\"\"]]")]
- public void EnsureEnrolledAddsPolicies(string groupPolicies)
+ public void AddPoliciesAddsPolicies(string groupPolicies)
{
// Arrange.
var group = new UserSecurityPolicyGroup()
@@ -103,7 +103,7 @@ public void EnsureEnrolledAddsPolicies(string groupPolicies)
[Theory]
[InlineData("[[\"A\",\"\"]]", "[[\"A\",null]]", 0)]
[InlineData("[[\"A\",\"B\"],[\"E\",\"\"]]", "[[\"E\",\"\"],[\"A\",\"B\"],[\"C\",\"D\"]]", 1)]
- public void EnsureUnEnrolledAddsPolicies(string groupPolicies, string userPolicies, int expectedCount)
+ public void RemovePoliciesRemovesPolicies(string groupPolicies, string userPolicies, int expectedCount)
{
// Arrange.
var group = new UserSecurityPolicyGroup()
From c3bc293c8611b4925ee0e5ce4680bf1e026be1b4 Mon Sep 17 00:00:00 2001
From: Christy Henriksson
Date: Tue, 2 May 2017 11:14:31 -0700
Subject: [PATCH 4/9] PR feedback
---
.../Areas/Admin/Controllers/SecurityPolicyController.cs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
index ec7b615854..ba767c572f 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
@@ -100,8 +100,9 @@ public async Task Enroll(SecurityPolicyViewModel viewModel)
private static string[] GetUsernamesFromQuery(string query)
{
- return query.Split(new[] { ',', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(username => username.Trim()).ToArray();
+ return query.Split(',', '\r', '\n')
+ .Select(username => username.Trim())
+ .Where(username => !string.IsNullOrEmpty(username)).ToArray();
}
private IEnumerable FindUsers(string[] usernames)
From e334a41f6d2429324053b4fa2c83d86a31f8314a Mon Sep 17 00:00:00 2001
From: Christy Henriksson
Date: Tue, 2 May 2017 11:35:43 -0700
Subject: [PATCH 5/9] Fix test failure with ToList
---
src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs b/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
index e15768d30d..48b653f28a 100644
--- a/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
+++ b/src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
@@ -56,15 +56,15 @@ public static void AddPolicies(this User user, UserSecurityPolicyGroup policyGro
public static IEnumerable RemovePolicies(this User user, UserSecurityPolicyGroup policyGroup)
{
// Remove policies, only if enrolled in all group policies.
- var matches = user.FindPolicies(policyGroup);
+ var matches = user.FindPolicies(policyGroup).ToList();
if (!matches.Any(p => p == null))
{
foreach (var policy in matches)
{
user.SecurityPolicies.Remove(policy);
- yield return policy;
}
}
+ return matches;
}
///
From a89ad4227a7c354ae890db50b61783a40f30da2e Mon Sep 17 00:00:00 2001
From: Christy Henriksson
Date: Tue, 2 May 2017 14:27:13 -0700
Subject: [PATCH 6/9] PR feedback
---
.../Controllers/SecurityPolicyController.cs | 12 +++--
.../Admin/Views/SecurityPolicy/Index.cshtml | 2 +-
.../RequireMinClientVersionForPushPolicy.cs | 12 +++++
.../Security/UserSecurityPolicyGroup.cs | 16 +++++--
.../Security/UserSecurityPolicyGroupFacts.cs | 48 +++++++++++--------
5 files changed, 59 insertions(+), 31 deletions(-)
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
index ba767c572f..e346616ad2 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
@@ -6,6 +6,8 @@
using System.Linq;
using System.Threading.Tasks;
using System.Web.Mvc;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using NuGetGallery.Areas.Admin.ViewModels;
using NuGetGallery.Security;
@@ -62,16 +64,16 @@ public virtual ActionResult Search(string query)
[ValidateAntiForgeryToken]
public async Task Enroll(SecurityPolicyViewModel viewModel)
{
- // Parse 'username|policyGroup' into enrollment requests by user.
+ // Group enrollment requests by user.
var enrollments = viewModel.Enrollments?
- .Select(e => e.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
- .GroupBy(e => /*username*/e[0])
+ .Select(json => JsonConvert.DeserializeObject(json))
+ .GroupBy(obj => obj["u"].ToString())
.ToDictionary(
g => g.Key,
- g => g.Select(e => /*policyGroup*/e[1])
+ g => g.Select(obj => obj["g"].ToString())
);
- // Iterate all users and groups for enrollment or unenrollment.
+ // Iterate all users and groups to handle both enrollment and unenrollment.
var usernames = GetUsernamesFromQuery(viewModel.Query);
var users = FindUsers(usernames);
foreach (var user in users)
diff --git a/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml
index 431caf8faf..b1ea6737cd 100644
--- a/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml
+++ b/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml
@@ -128,7 +128,7 @@
};
this.generateValue = function (user, policyGroup) {
- return user.Username + '|' + policyGroup;
+ return JSON.stringify({ "u": user.Username, "g": policyGroup })
};
this.generateUserUrl = function (user) {
diff --git a/src/NuGetGallery/Security/RequireMinClientVersionForPushPolicy.cs b/src/NuGetGallery/Security/RequireMinClientVersionForPushPolicy.cs
index f6ca242159..4ebf5027ab 100644
--- a/src/NuGetGallery/Security/RequireMinClientVersionForPushPolicy.cs
+++ b/src/NuGetGallery/Security/RequireMinClientVersionForPushPolicy.cs
@@ -30,6 +30,18 @@ public RequireMinClientVersionForPushPolicy()
{
}
+ ///
+ /// Create a user security policy that requires a minimum client version.
+ ///
+ public static UserSecurityPolicy CreatePolicy(NuGetVersion minClientVersion)
+ {
+ var value = JsonConvert.SerializeObject(new State() {
+ MinClientVersion = minClientVersion
+ });
+
+ return new UserSecurityPolicy(PolicyName, value);
+ }
+
///
/// In case of multiple, select the max of the minimum required client versions.
///
diff --git a/src/NuGetGallery/Security/UserSecurityPolicyGroup.cs b/src/NuGetGallery/Security/UserSecurityPolicyGroup.cs
index 4c607513d1..d1f804ad10 100644
--- a/src/NuGetGallery/Security/UserSecurityPolicyGroup.cs
+++ b/src/NuGetGallery/Security/UserSecurityPolicyGroup.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using NuGet.Versioning;
using NuGetGallery.Authentication;
namespace NuGetGallery.Security
@@ -14,6 +15,7 @@ namespace NuGetGallery.Security
public class UserSecurityPolicyGroup
{
public const string SecurePush = "SecurePush";
+ public const string SecurePushVersion = "4.1.0";
///
/// Policy group name.
@@ -51,10 +53,10 @@ private static IEnumerable CreateUserPolicyGroups()
yield return new UserSecurityPolicyGroup()
{
Name = UserSecurityPolicyGroup.SecurePush,
- Policies = new []
+ Policies = new[]
{
new UserSecurityPolicy(RequirePackageVerifyScopePolicy.PolicyName),
- new UserSecurityPolicy(RequireMinClientVersionForPushPolicy.PolicyName) { Value = "{\"v\":\"4.1.0\"}" }
+ RequireMinClientVersionForPushPolicy.CreatePolicy(new NuGetVersion(SecurePushVersion))
},
OnEnroll = OnEnroll_SecurePush
};
@@ -63,7 +65,7 @@ private static IEnumerable CreateUserPolicyGroups()
private static void OnEnroll_SecurePush(User user)
{
var pushKeys = user.Credentials.Where(c =>
- c.Type.StartsWith(CredentialTypes.ApiKey.Prefix) && !c.HasExpired &&
+ c.Type.StartsWith(CredentialTypes.ApiKey.Prefix) &&
(
c.Scopes.Count == 0 ||
c.Scopes.Any(s =>
@@ -71,10 +73,14 @@ private static void OnEnroll_SecurePush(User user)
s.AllowedAction.Equals(NuGetScopes.PackagePushVersion, StringComparison.OrdinalIgnoreCase)
))
);
-
+
foreach (var key in pushKeys)
{
- key.Expires = DateTime.Now.AddDays(7);
+ var maxExpiration = DateTime.UtcNow.AddDays(7);
+ if (!key.Expires.HasValue || key.Expires > maxExpiration)
+ {
+ key.Expires = maxExpiration;
+ }
}
}
}
diff --git a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
index e0b606a78b..80fa8fc704 100644
--- a/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
+++ b/tests/NuGetGallery.Facts/Security/UserSecurityPolicyGroupFacts.cs
@@ -7,7 +7,6 @@
using Newtonsoft.Json;
using NuGet.Packaging;
using Xunit;
-using System.Web.Mvc;
namespace NuGetGallery.Security
{
@@ -16,24 +15,11 @@ public class UserSecurityPolicyGroupFacts
[Theory]
[InlineData("")]
[InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]")]
- [InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]")]
[InlineData("[{\"a\":\"package:pushversion\", \"s\":\"theId\"}]")]
public void SecurePush_OnEnrollExpiresPushApiKeys(string scopes)
{
- // Arrange.
- var group = UserSecurityPolicyGroup.Instances.First(
- g => g.Name.Equals(UserSecurityPolicyGroup.SecurePush, StringComparison.OrdinalIgnoreCase));
-
- var credential = new Credential(CredentialTypes.ApiKey.V2, string.Empty, TimeSpan.FromDays(10));
- if (!string.IsNullOrWhiteSpace(scopes))
- {
- credential.Scopes.AddRange(JsonConvert.DeserializeObject>(scopes));
- }
- var user = new User();
- user.Credentials.Add(credential);
-
- // Act.
- user.AddPolicies(group);
+ // Arrange & Act.
+ var user = EnrollUserInSecurePush(CredentialTypes.ApiKey.V2, scopes);
// Assert.
Assert.Equal(2, user.SecurityPolicies.Count());
@@ -45,12 +31,36 @@ public void SecurePush_OnEnrollExpiresPushApiKeys(string scopes)
[InlineData("apikey.v2", "[{\"a\":\"package:unlist\", \"s\":\"theId\"}]")]
[InlineData("apikey.verify.v1", "[{\"a\":\"package:verify\", \"s\":\"theId\"}]")]
public void SecurePush_OnEnrollDoesNotExpireNonPushCredentials(string type, string scopes)
+ {
+ // Arrange & Act.
+ var user = EnrollUserInSecurePush(type, scopes);
+
+ // Assert.
+ Assert.Equal(2, user.SecurityPolicies.Count());
+ Assert.False(DateTime.UtcNow.AddDays(7) >= user.Credentials.First().Expires);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]")]
+ [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"theId\"}]")]
+ public void SecurePush_OnEnrollDoesNotChangeExpiringPushCredentials(string scopes)
+ {
+ // Arrange & Act.
+ var user = EnrollUserInSecurePush(CredentialTypes.ApiKey.V2, scopes, expiresInDays: 2);
+
+ // Assert.
+ Assert.Equal(2, user.SecurityPolicies.Count());
+ Assert.True(DateTime.UtcNow.AddDays(2) >= user.Credentials.First().Expires);
+ }
+
+ private User EnrollUserInSecurePush(string type, string scopes, int expiresInDays = 10)
{
// Arrange.
var group = UserSecurityPolicyGroup.Instances.First(
g => g.Name.Equals(UserSecurityPolicyGroup.SecurePush, StringComparison.OrdinalIgnoreCase));
- var credential = new Credential(type, string.Empty, TimeSpan.FromDays(10));
+ var credential = new Credential(type, string.Empty, TimeSpan.FromDays(expiresInDays));
if (!string.IsNullOrWhiteSpace(scopes))
{
credential.Scopes.AddRange(JsonConvert.DeserializeObject>(scopes));
@@ -61,9 +71,7 @@ public void SecurePush_OnEnrollDoesNotExpireNonPushCredentials(string type, stri
// Act.
user.AddPolicies(group);
- // Assert.
- Assert.Equal(2, user.SecurityPolicies.Count());
- Assert.False(DateTime.UtcNow.AddDays(7) >= user.Credentials.First().Expires);
+ return user;
}
}
}
From 3f840d33a195cce8e65322fd2aba008396d6eee1 Mon Sep 17 00:00:00 2001
From: Christy Henriksson
Date: Thu, 4 May 2017 14:56:00 -0700
Subject: [PATCH 7/9] PR feedback
---
.../Entities/UserSecurityPolicy.cs | 45 ++-
.../Controllers/SecurityPolicyController.cs | 55 +--
.../ViewModels/SecurityPolicySearchResult.cs | 38 --
.../ViewModels/SecurityPolicyViewModel.cs | 8 +-
.../UserSecurityPolicySearchResult.cs | 23 ++
.../UserSecurityPolicySubscriptions.cs | 22 ++
.../Admin/Views/SecurityPolicy/Index.cshtml | 50 +--
src/NuGetGallery/ExtensionMethods.cs | 3 +-
...ityPolicies_SubscriptionColumn.Designer.cs | 29 ++
...UserSecurityPolicies_SubscriptionColumn.cs | 18 +
...erSecurityPolicies_SubscriptionColumn.resx | 126 ++++++
src/NuGetGallery/NuGetGallery.csproj | 14 +-
.../Security/ISecurityPolicyService.cs | 24 +-
.../IUserSecurityPolicySubscription.cs | 34 ++
.../RequireMinClientVersionForPushPolicy.cs | 7 +-
.../Security/SecurePushSubscription.cs | 77 ++++
.../Security/SecurityPolicyService.cs | 146 ++++++-
.../Security/UserSecurityPolicyExtensions.cs | 78 ----
.../Security/UserSecurityPolicyGroup.cs | 87 -----
.../Entities/UserSecurityPolicyFacts.cs | 70 ++++
.../NuGetGallery.Core.Facts.csproj | 1 +
.../NuGetGallery.Facts.csproj | 3 +-
...quireMinClientVersionForPushPolicyFacts.cs | 33 +-
.../RequirePackageVerifyScopePolicyFacts.cs | 2 +-
...acts.cs => SecurePushSubscriptionFacts.cs} | 50 ++-
.../Security/SecurityPolicyServiceFacts.cs | 364 ++++++++++++++++--
.../UserSecurityPolicyExtensionsFacts.cs | 135 -------
27 files changed, 1054 insertions(+), 488 deletions(-)
delete mode 100644 src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicySearchResult.cs
create mode 100644 src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySearchResult.cs
create mode 100644 src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySubscriptions.cs
create mode 100644 src/NuGetGallery/Migrations/201705041614287_UserSecurityPolicies_SubscriptionColumn.Designer.cs
create mode 100644 src/NuGetGallery/Migrations/201705041614287_UserSecurityPolicies_SubscriptionColumn.cs
create mode 100644 src/NuGetGallery/Migrations/201705041614287_UserSecurityPolicies_SubscriptionColumn.resx
create mode 100644 src/NuGetGallery/Security/IUserSecurityPolicySubscription.cs
create mode 100644 src/NuGetGallery/Security/SecurePushSubscription.cs
delete mode 100644 src/NuGetGallery/Security/UserSecurityPolicyExtensions.cs
delete mode 100644 src/NuGetGallery/Security/UserSecurityPolicyGroup.cs
create mode 100644 tests/NuGetGallery.Core.Facts/Entities/UserSecurityPolicyFacts.cs
rename tests/NuGetGallery.Facts/Security/{UserSecurityPolicyGroupFacts.cs => SecurePushSubscriptionFacts.cs} (50%)
delete mode 100644 tests/NuGetGallery.Facts/Security/UserSecurityPolicyExtensionsFacts.cs
diff --git a/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs b/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs
index 649b8372c2..d935b43d51 100644
--- a/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs
+++ b/src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.ComponentModel.DataAnnotations;
namespace NuGetGallery
@@ -8,22 +9,34 @@ namespace NuGetGallery
///
/// User-subscribed security policy.
///
- public class UserSecurityPolicy : IEntity
+ public class UserSecurityPolicy : IEntity, IEquatable
{
public UserSecurityPolicy()
{
}
- public UserSecurityPolicy(string name)
+ public UserSecurityPolicy(string name, string subscription, string value = null)
{
- Name = name;
- }
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+ if (string.IsNullOrEmpty(subscription))
+ {
+ throw new ArgumentNullException(nameof(subscription));
+ }
- public UserSecurityPolicy(string name, string value)
- {
Name = name;
+ Subscription = subscription;
Value = value;
}
+
+ public UserSecurityPolicy(UserSecurityPolicy policy)
+ {
+ Name = policy.Name;
+ Subscription = policy.Subscription;
+ Value = policy.Value;
+ }
///
/// Policy key.
@@ -47,9 +60,29 @@ public UserSecurityPolicy(string name, string value)
[StringLength(256)]
public string Name { get; set; }
+ ///
+ /// Name of subscription that added this policy.
+ ///
+ [Required]
+ [StringLength(256)]
+ public string Subscription { get; set; }
+
///
/// Support for JSON-serialized properties for specific policies.
///
public string Value { get; set; }
+
+ ///
+ /// Determine if two policies are equal.
+ ///
+ public bool Equals(UserSecurityPolicy other)
+ {
+ return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) &&
+ Subscription.Equals(other.Subscription, StringComparison.OrdinalIgnoreCase) &&
+ (
+ (string.IsNullOrEmpty(Value) && string.IsNullOrEmpty(other.Value)) ||
+ (Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase))
+ );
+ }
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
index e346616ad2..8e1954b77a 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
@@ -13,13 +13,28 @@
namespace NuGetGallery.Areas.Admin.Controllers
{
+ ///
+ /// Controller for the security policy management Admin view.
+ ///
public class SecurityPolicyController : AdminControllerBase
{
public IEntitiesContext EntitiesContext { get; }
- public SecurityPolicyController(IEntitiesContext entitiesContext)
+ public ISecurityPolicyService PolicyService { get; }
+
+ public SecurityPolicyController(IEntitiesContext entitiesContext, ISecurityPolicyService policyService)
{
+ if (entitiesContext == null)
+ {
+ throw new ArgumentNullException(nameof(entitiesContext));
+ }
+ if (policyService == null)
+ {
+ throw new ArgumentNullException(nameof(policyService));
+ }
+
EntitiesContext = entitiesContext;
+ PolicyService = policyService;
}
[HttpGet]
@@ -27,7 +42,7 @@ public virtual ActionResult Index()
{
var model = new SecurityPolicyViewModel()
{
- PolicyGroups = UserSecurityPolicyGroup.Instances.Select(pg => pg.Name)
+ SubscriptionNames = PolicyService.UserSubscriptions.Select(s => s.Name)
};
return View(model);
@@ -43,15 +58,15 @@ public virtual ActionResult Search(string query)
.Where(name => !users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
.ToList();
- var results = new SecurityPolicySearchResult()
+ var results = new UserSecurityPolicySearchResult()
{
- // Found users and enrollment status for each policy group.
- Users = users.Select(u => new SecurityPolicyEnrollments()
+ // Found users and subscribed status for each policy subscription.
+ Users = users.Select(u => new UserSecurityPolicySubscriptions()
{
Username = u.Username,
- Enrollments = UserSecurityPolicyGroup.Instances.ToDictionary(
- pg => pg.Name,
- pg => u.IsEnrolled(pg))
+ Subscriptions = PolicyService.UserSubscriptions.ToDictionary(
+ s => s.Name,
+ s => PolicyService.IsSubscribed(u, s))
}),
// Usernames that weren't found in the DB.
UsersNotFound = usersNotFound
@@ -62,10 +77,10 @@ public virtual ActionResult Search(string query)
[HttpPost]
[ValidateAntiForgeryToken]
- public async Task Enroll(SecurityPolicyViewModel viewModel)
+ public async Task Update(SecurityPolicyViewModel viewModel)
{
- // Group enrollment requests by user.
- var enrollments = viewModel.Enrollments?
+ // Policy subscription requests by user.
+ var subscriptions = viewModel.UserSubscriptions?
.Select(json => JsonConvert.DeserializeObject(json))
.GroupBy(obj => obj["u"].ToString())
.ToDictionary(
@@ -73,29 +88,25 @@ public async Task Enroll(SecurityPolicyViewModel viewModel)
g => g.Select(obj => obj["g"].ToString())
);
- // Iterate all users and groups to handle both enrollment and unenrollment.
- var usernames = GetUsernamesFromQuery(viewModel.Query);
+ // Iterate all users and groups to handle both subscribe and unsubscribe.
+ var usernames = GetUsernamesFromQuery(viewModel.UsersQuery);
var users = FindUsers(usernames);
foreach (var user in users)
{
- foreach (var policyGroup in UserSecurityPolicyGroup.Instances)
+ foreach (var subscription in PolicyService.UserSubscriptions)
{
- if (enrollments != null && enrollments[user.Username].Contains(policyGroup.Name))
+ if (subscriptions != null && subscriptions[user.Username].Contains(subscription.Name))
{
- user.AddPolicies(policyGroup);
+ await PolicyService.SubscribeAsync(user, subscription);
}
else
{
- var removedPolicies = user.RemovePolicies(policyGroup);
- foreach (var p in removedPolicies)
- {
- EntitiesContext.UserSecurityPolicies.Remove(p);
- }
+ await PolicyService.UnsubscribeAsync(user, subscription);
}
}
}
- await EntitiesContext.SaveChangesAsync();
+ TempData["Message"] = $"Updated policies for {users.Count()} users.";
return RedirectToAction("Index");
}
diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicySearchResult.cs b/src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicySearchResult.cs
deleted file mode 100644
index c79b485ac8..0000000000
--- a/src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicySearchResult.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System.Collections.Generic;
-
-namespace NuGetGallery.Areas.Admin.ViewModels
-{
- ///
- /// User search results for the security policies admin view.
- ///
- public class SecurityPolicySearchResult
- {
- ///
- /// Found users, with security policy group enrollments.
- ///
- public IEnumerable Users { get; set; }
-
- ///
- /// Usernames not found in the database.
- ///
- public IEnumerable UsersNotFound { get; set; }
- }
-
- ///
- /// Security policy group enrollments for a user.
- ///
- public class SecurityPolicyEnrollments
- {
- public int UserId { get; set; }
-
- public string Username { get; set; }
-
- ///
- /// Dictionary of security policy group names (key) and whether the user is enrolled.
- ///
- public IDictionary Enrollments { get; set; }
- }
-}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicyViewModel.cs b/src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicyViewModel.cs
index e559fcb23d..14a517b57c 100644
--- a/src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicyViewModel.cs
+++ b/src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicyViewModel.cs
@@ -13,16 +13,16 @@ public class SecurityPolicyViewModel
///
/// Users search query.
///
- public string Query { get; set; }
+ public string UsersQuery { get; set; }
///
/// Available security policy groups.
///
- public IEnumerable PolicyGroups { get; set; }
+ public IEnumerable SubscriptionNames { get; set; }
///
- /// Requested user enrollments to make. String format is 'username|policygroup'.
+ /// User subscription requests, in JSON format.
///
- public IEnumerable Enrollments { get; set; }
+ public IEnumerable UserSubscriptions { get; set; }
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySearchResult.cs b/src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySearchResult.cs
new file mode 100644
index 0000000000..b221e5944c
--- /dev/null
+++ b/src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySearchResult.cs
@@ -0,0 +1,23 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace NuGetGallery.Areas.Admin.ViewModels
+{
+ ///
+ /// User search results for the security policies admin view.
+ ///
+ public class UserSecurityPolicySearchResult
+ {
+ ///
+ /// Found users, with security policy subscriptions they are subscribed to.
+ ///
+ public IEnumerable Users { get; set; }
+
+ ///
+ /// Usernames not found in the database.
+ ///
+ public IEnumerable UsersNotFound { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySubscriptions.cs b/src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySubscriptions.cs
new file mode 100644
index 0000000000..877adfbad5
--- /dev/null
+++ b/src/NuGetGallery/Areas/Admin/ViewModels/UserSecurityPolicySubscriptions.cs
@@ -0,0 +1,22 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace NuGetGallery.Areas.Admin.ViewModels
+{
+ ///
+ /// Security policy group subscriptions for a user.
+ ///
+ public class UserSecurityPolicySubscriptions
+ {
+ public int UserId { get; set; }
+
+ public string Username { get; set; }
+
+ ///
+ /// Dictionary of security policy subscriptions, and whether user is subscribed.
+ ///
+ public IDictionary Subscriptions { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml
index b1ea6737cd..a274127cee 100644
--- a/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml
+++ b/src/NuGetGallery/Areas/Admin/Views/SecurityPolicy/Index.cshtml
@@ -13,10 +13,10 @@
- @using (Html.BeginForm("Enroll", "SecurityPolicy", new { area = "Admin" }, FormMethod.Post, new { id = "delete-form" }))
+ @using (Html.BeginForm("Update", "SecurityPolicy", new { area = "Admin" }, FormMethod.Post, new { id = "delete-form" }))
{
-
+
The following users were not found:
@@ -28,18 +28,18 @@
Username
- @foreach (var policyGroup in Model.PolicyGroups)
+ @foreach (var subscription in Model.SubscriptionNames)
{
-
@policyGroup
+
@subscription
}
- @foreach (var policyGroup in Model.PolicyGroups)
+ @foreach (var subscription in Model.SubscriptionNames)
{
-
+
}
@@ -52,10 +52,10 @@
@Html.AntiForgeryToken()
- User enrollment in security policies could result in changes which CANNOT be undone.
+ Onboarding users to security policy subscriptions could result in changes which CANNOT be undone.