Skip to content

Commit

Permalink
Temp keys policy onboarding (#3854)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenriksson committed May 16, 2017
1 parent 9ea3066 commit a1bc005
Show file tree
Hide file tree
Showing 30 changed files with 1,662 additions and 95 deletions.
1 change: 1 addition & 0 deletions src/NuGetGallery.Core/Entities/EntitiesContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public EntitiesContext(string connectionString, bool readOnly)
public IDbSet<Credential> Credentials { get; set; }
public IDbSet<Scope> Scopes { get; set; }
public IDbSet<User> Users { get; set; }
public IDbSet<UserSecurityPolicy> UserSecurityPolicies { get; set; }

IDbSet<T> IEntitiesContext.Set<T>()
{
Expand Down
3 changes: 2 additions & 1 deletion src/NuGetGallery.Core/Entities/IEntitiesContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ public interface IEntitiesContext
IDbSet<PackageRegistration> PackageRegistrations { get; set; }
IDbSet<Credential> Credentials { get; set; }
IDbSet<Scope> Scopes { get; set; }

IDbSet<User> Users { get; set; }
IDbSet<UserSecurityPolicy> UserSecurityPolicies { get; set; }

Task<int> SaveChangesAsync();
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification="This is to match the EF terminology.")]
IDbSet<T> Set<T>() where T : class;
Expand Down
42 changes: 39 additions & 3 deletions src/NuGetGallery.Core/Entities/UserSecurityPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
// 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
{
/// <summary>
/// User-subscribed security policy.
/// </summary>
public class UserSecurityPolicy : IEntity
public class UserSecurityPolicy : IEntity, IEquatable<UserSecurityPolicy>
{
public UserSecurityPolicy()
{
}

public UserSecurityPolicy(string name)
public UserSecurityPolicy(UserSecurityPolicy policy)
: this(policy.Name, policy.Subscription, policy.Value)
{
Name = name;
}

public UserSecurityPolicy(string name, string subscription, string value = null)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Subscription = subscription ?? throw new ArgumentNullException(nameof(subscription));
Value = value;
}

/// <summary>
Expand All @@ -41,9 +49,37 @@ public UserSecurityPolicy(string name)
[StringLength(256)]
public string Name { get; set; }

/// <summary>
/// Name of subscription that added this policy.
/// </summary>
[Required]
[StringLength(256)]
public string Subscription { get; set; }

/// <summary>
/// Support for JSON-serialized properties for specific policies.
/// </summary>
public string Value { get; set; }

/// <summary>
/// Determine if two policies are equal.
/// </summary>
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))
);
}

private static readonly Func<object, long, long> _hash = (i, hash) => ((hash << 5) + hash) ^ (i?.GetHashCode() ?? 0);
private const long _seed = 0x1505L;

public override int GetHashCode()
{
return _hash(Value, _hash(Subscription, _hash(Name, _seed))).GetHashCode();
}
}
}
122 changes: 122 additions & 0 deletions src/NuGetGallery/Areas/Admin/Controllers/SecurityPolicyController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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 System.Threading.Tasks;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NuGetGallery.Areas.Admin.ViewModels;
using NuGetGallery.Security;

namespace NuGetGallery.Areas.Admin.Controllers
{
/// <summary>
/// Controller for the security policy management Admin view.
/// </summary>
public class SecurityPolicyController : AdminControllerBase
{
protected IEntitiesContext EntitiesContext { get; set; }

protected ISecurityPolicyService PolicyService { get; set; }

protected SecurityPolicyController()
{
}

public SecurityPolicyController(IEntitiesContext entitiesContext, ISecurityPolicyService policyService)
{
EntitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext));
PolicyService = policyService ?? throw new ArgumentNullException(nameof(policyService));
}

[HttpGet]
public virtual ActionResult Index()
{
var model = new SecurityPolicyViewModel()
{
SubscriptionNames = PolicyService.UserSubscriptions.Select(s => s.SubscriptionName)
};

return View(model);
}

[HttpGet]
public virtual JsonResult Search(string query)
{
// Parse query and look for users in the DB.
var usernames = GetUsernamesFromQuery(query ?? "");
var users = FindUsers(usernames);
var usersNotFound = usernames.Except(users.Select(u => u.Username));

var results = new UserSecurityPolicySearchResult()
{
// Found users and subscribed status for each policy subscription.
Users = users.Select(u => new UserSecurityPolicySubscriptions()
{
Username = u.Username,
Subscriptions = PolicyService.UserSubscriptions.ToDictionary(
s => s.SubscriptionName,
s => PolicyService.IsSubscribed(u, s))
}),
// Usernames that weren't found in the DB.
UsersNotFound = usersNotFound
};

return Json(results, JsonRequestBehavior.AllowGet);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Update(SecurityPolicyViewModel viewModel)
{
// Policy subscription requests by user.
var subscriptions = viewModel.UserSubscriptions?
.Select(json => JsonConvert.DeserializeObject<JObject>(json))
.GroupBy(obj => obj["u"].ToString())
.ToDictionary(
g => g.Key,
g => g.Select(obj => obj["g"].ToString())
);

// 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 subscription in PolicyService.UserSubscriptions)
{
var userKeyExists = subscriptions?.ContainsKey(user.Username) ?? false;
if (userKeyExists && subscriptions[user.Username].Contains(subscription.SubscriptionName))
{
await PolicyService.SubscribeAsync(user, subscription);
}
else
{
await PolicyService.UnsubscribeAsync(user, subscription);
}
}
}

TempData["Message"] = $"Updated policies for {users.Count()} users.";

return RedirectToAction("Index");
}

private static string[] GetUsernamesFromQuery(string query)
{
return query.Split(',', '\r', '\n')
.Select(username => username.Trim())
.Where(username => !string.IsNullOrEmpty(username)).ToArray();
}

private IEnumerable<User> FindUsers(string[] usernames)
{
return EntitiesContext.Users
.Where(u => usernames.Any(name => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
.ToList();
}
}
}
28 changes: 28 additions & 0 deletions src/NuGetGallery/Areas/Admin/ViewModels/SecurityPolicyViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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
{
/// <summary>
/// View model for the security policies admin view.
/// </summary>
public class SecurityPolicyViewModel
{
/// <summary>
/// Users search query.
/// </summary>
public string UsersQuery { get; set; }

/// <summary>
/// Available security policy groups.
/// </summary>
public IEnumerable<string> SubscriptionNames { get; set; }

/// <summary>
/// User subscription requests, in JSON format.
/// </summary>
public IEnumerable<string> UserSubscriptions { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// User search results for the security policies admin view.
/// </summary>
public class UserSecurityPolicySearchResult
{
/// <summary>
/// Found users, with security policy subscriptions they are subscribed to.
/// </summary>
public IEnumerable<UserSecurityPolicySubscriptions> Users { get; set; }

/// <summary>
/// Usernames not found in the database.
/// </summary>
public IEnumerable<string> UsersNotFound { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Security policy group subscriptions for a user.
/// </summary>
public class UserSecurityPolicySubscriptions
{
public int UserId { get; set; }

public string Username { get; set; }

/// <summary>
/// Dictionary of security policy subscriptions, and whether user is subscribed.
/// </summary>
public IDictionary<string, bool> Subscriptions { get; set; }
}
}
11 changes: 11 additions & 0 deletions src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,16 @@
Clear Content Cache
</p>
</li>
<li>
<h2>
<a class="action-menu-link" href="@Url.Action(actionName: "Index", controllerName: "SecurityPolicy")">
<i class="icon-user action-menu-icon"></i>
<span class="action-menu-text">Security Policies</span>
</a>
</h2>
<p>
Manage User Security Policies
</p>
</li>
</ul>
</section>
Loading

0 comments on commit a1bc005

Please sign in to comment.