Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Temp keys policy onboarding #3854

Merged
merged 9 commits into from
May 8, 2017
Merged

Conversation

chenriksson
Copy link
Member

No description provided.

private static string[] GetUsernamesFromQuery(string query)
{
return query.Split(new[] { ',', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(username => username.Trim()).ToArray();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably filter out empty results after trimming.

private IEnumerable<User> FindUsers(string[] usernames)
{
return EntitiesContext.Users
.Where(u => usernames.Any(name => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This properly becomes Username IN ('a', 'b', ...) in SQL? Or is this being evaluated client side?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evaluated SQL-side, with similar query...

SELECT * FROM Users WHERE EXISTS(
  SELECT 1 AS [C1] FROM  (
    SELECT N'name1' AS [C1] FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]
    UNION ALL
    SELECT N'name2' AS [C1] FROM  ( SELECT 1 AS X ) AS [SingleRowTable2]
) AS [UnionAll2] WHERE Users.[Username] = [UnionAll2].[C1])

ViewBag.Title = "Security Policies";
}

<section>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we get a screenshot?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you send everybody?


In reply to: 114229583 [](ancestors = 114229583)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can upload the image to GitHub so everybody can see.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attached to conversation

return true;
};

this.generateValue = function (user, policyGroup) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really have to invent our own serialization here? Can't we just JSON encode?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Policies = new []
{
new UserSecurityPolicy(RequirePackageVerifyScopePolicy.PolicyName),
new UserSecurityPolicy(RequireMinClientVersionForPushPolicy.PolicyName) { Value = "{\"v\":\"4.1.0\"}" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use JSON API to write this JSON string.


foreach (var key in pushKeys)
{
key.Expires = DateTime.Now.AddDays(7);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fall into the typical expiration notification flow -- so user's might now know reason why this is expiring, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, DateTime.UtcNow

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could actually give some keys more time right? Shouldn't we only do this if the Expires is greater than 7 days in the future?


private static IEnumerable<UserSecurityPolicyGroup> CreateUserPolicyGroups()
{
yield return new UserSecurityPolicyGroup()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is all constant, can't we just subclass UserSecurityPolicyGroup? This allows us to avoid the Action<User> and just make it an overridden method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is really confusing as is...just make this an abstract class with SecurePushUserSecurityPolicyGroup a subclass with all of this logic.

/// <summary>
/// Grouping of one or more user security policies for enrollment.
/// </summary>
public class UserSecurityPolicyGroup
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me like a "policy group" is a bit redundant. Isn't this just another policy, a la "aggregate policy"? Maybe I'm off here but couldn't a policy either have it's own logic, be an aggregate of other policies, or both? I don't see why we need another type for a grouping when the UserSecurityPolicy type could describe it as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joelverhagen @skofman1 per our discussion, will add a new column to UserSecurityPolicies table to track what was enrolled in - which could be a single policy or aggregate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chenriksson make sure that column handles the state of being enrolled in multiple aggregates.

e.g.
I'm enrolled in policy A through groups X and Y. I leave group X. I should still be enrolled in policy A through group Y.

Copy link
Member

@joelverhagen joelverhagen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⌚️


public SecurityPolicyController(IEntitiesContext entitiesContext)
{
EntitiesContext = entitiesContext;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null check

/// Get supported user security policy groups.
/// </summary>
private static List<UserSecurityPolicyGroup> _instances;
public static List<UserSecurityPolicyGroup> Instances
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider using Lazy

private static void OnEnroll_SecurePush(User user)
{
var pushKeys = user.Credentials.Where(c =>
c.Type.StartsWith(CredentialTypes.ApiKey.Prefix) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c.Type.StartsWith(CredentialTypes.ApiKey.Prefi [](start = 16, length = 46)

I think there is a method IsApiKey that has this logic

/// <summary>
/// Security policy group enrollments for a user.
/// </summary>
public class SecurityPolicyEnrollments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r [](start = 21, length = 1)

one class per file please

}
}

await EntitiesContext.SaveChangesAsync();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we send a user an e-mail when he is enrolled? Perhaps Anand has ideas.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skofman1
Copy link
Contributor

skofman1 commented May 3, 2017

:shipit:

Copy link
Contributor

@scottbommarito scottbommarito left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really want to see UserSecurityPolicyGroup split into subclasses. Otherwise looks good.

/// <summary>
/// Security policy group enrollments for a user.
/// </summary>
public class SecurityPolicyEnrollments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be more clear if this was named UserSecurityPolicyEnrollments.

ViewBag.Title = "Security Policies";
}

<section>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can upload the image to GitHub so everybody can see.

public IEnumerable<string> PolicyGroups { get; set; }

/// <summary>
/// Requested user enrollments to make. String format is 'username|policygroup'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you changed the serialization did you update this comment?

/// <summary>
/// Grouping of one or more user security policies for enrollment.
/// </summary>
public class UserSecurityPolicyGroup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chenriksson make sure that column handles the state of being enrolled in multiple aggregates.

e.g.
I'm enrolled in policy A through groups X and Y. I leave group X. I should still be enrolled in policy A through group Y.


private static IEnumerable<UserSecurityPolicyGroup> CreateUserPolicyGroups()
{
yield return new UserSecurityPolicyGroup()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is really confusing as is...just make this an abstract class with SecurePushUserSecurityPolicyGroup a subclass with all of this logic.

{
[Theory]
[InlineData("", "")]
[InlineData(null, "")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also check [InlineData("", null)] just in case

/// <summary>
/// Determine whether two security policies are equivalent.
/// </summary>
public static bool Matches(this UserSecurityPolicy first, UserSecurityPolicy second)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not an Equals method?

e.g. UserSecurityPolicy should implement IEquatable<UserSecurityPolicy>

// Arrange.
var group = new UserSecurityPolicyGroup()
{
Policies = LoadPolicies(groupPolicies)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use MemberData instead of InlineData you can get rid of the serialization/deserialization.

[Theory]
[InlineData("[[\"A\",\"\"]]")]
[InlineData("[[\"A\",\"B\"],[\"E\",\"\"]]")]
public void AddPoliciesAddsPolicies(string groupPolicies)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want test cases around
1 - adding policies that are already added
2 - removing policies that are not enrolled


await EntitiesContext.SaveChangesAsync();

return RedirectToAction("Index");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually these kind of operations will set a message on the page, e.g. User x was enrolled in y.

Do we want to do that here? It could be nontrivial to figure out what was changed otherwise.

@chenriksson
Copy link
Member Author

@scottbommarito

policies

/// <summary>
/// Subscription name.
/// </summary>
public string Name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name [](start = 22, length = 4)

Nit: => SubscriptionName

using System.ComponentModel.DataAnnotations;

namespace NuGetGallery
{
/// <summary>
/// User-subscribed security policy.
/// </summary>
public class UserSecurityPolicy : IEntity
public class UserSecurityPolicy : IEntity, IEquatable<UserSecurityPolicy>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IEquatable [](start = 47, length = 10)

IEquatable should override GetHashCode

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Assert.True(first.Equals(second));
}

public static IEnumerable<UserSecurityPolicy[]> EqualsReturnsFalse_Data()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EqualsReturnsFalse_Data [](start = 56, length = 23)

nit make this a property

{
public override void Up()
{
AddColumn("dbo.UserSecurityPolicies", "Subscription", c => c.String(nullable: false, maxLength: 256));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nullable [](start = 80, length = 8)

What will existing records do with this?

Or are we assuming there is no data?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No data in the table yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Thanks.

{
public IEntitiesContext EntitiesContext { get; }

public ISecurityPolicyService PolicyService { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PolicyService [](start = 38, length = 13)

nit why public?

}

[HttpGet]
public virtual ActionResult Search(string query)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Search [](start = 36, length = 6)

thoughts on unit testing this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we could mock the context and policy service


In reply to: 115030333 [](ancestors = 115030333)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joelverhagen Tests added!

var usernames = GetUsernamesFromQuery(query);
var users = FindUsers(usernames);
var usersNotFound = usernames
.Where(name => !users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where [](start = 17, length = 5)

linq except might be a bit clearer for this

throw new ArgumentNullException(nameof(subscription));
}

if (!IsSubscribed(user, subscription))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsSubscribed [](start = 17, length = 12)

This isn't thread safe. Two simultaneous enrolls to the same subscription would result in duplicate policies right? Or is there a optimistic concurrency on the user?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid that's the case for a lot of our CUD actions... for scoped API keys I added "locks" in the page itself to prevent double clicks from creating duplicate calls.

Copy link
Member Author

@chenriksson chenriksson May 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confirmation prompt should help guard against duplicate clicks. I don't think a lock in the gallery (per instance) or optimistic concurrency in the DB (per row) is the solution here.

Now that we have the Subscription column, we could add a unique index (Name, Subscription)... assuming we wouldn't have a subscription contain multiple of the same policies differing in value only. We could then silently ignore duplicate inserts (SqlException 2601). Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Name, Subscription) unique index seems right. That being said, it seems like the "is enrolled?" and unenroll code paths handle duplicates just fine.

/// <summary>
/// Expire API keys with push capability on secure push enrollment.
/// </summary>
private static void SetPushApiKeysToExpire(User user, int expirationInDays = 7)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

= 7 [](start = 83, length = 3)

why optional parameter? there is only one acceptable value here today right? seems like a private const more correct here

@@ -14,14 +15,47 @@ namespace NuGetGallery.Security
/// </summary>
public class SecurityPolicyService : ISecurityPolicyService
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecurityPolicyService [](start = 17, length = 21)

I don't see any logging around these actions. Are we thinking that auditing is enough?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was planning to do auditing and telemetry together in next PR.

@joelverhagen
Copy link
Member

🕐


public SecurityPolicyController(IEntitiesContext entitiesContext, ISecurityPolicyService policyService)
{
if (entitiesContext == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I tried in an earlier commit and had a CI issue... but will try it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure you have the changes to build.ps1 from this commit. If it still fails, run it on the build machines queue and not the hosted agent queue.

@@ -461,8 +461,7 @@ public static Credential GetCurrentApiKeyCredential(this User user, IIdentity id
var claimsIdentity = identity as ClaimsIdentity;
var apiKey = claimsIdentity.GetClaimOrDefault(NuGetClaims.ApiKey);

return string.IsNullOrEmpty(apiKey) ? null
: user.Credentials.FirstOrDefault(c => c.Value == apiKey);
return user.Credentials.FirstOrDefault(c => c.Value == apiKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will be the behavior if we authenticated with a credential that is not an API key?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The apiKey var would be empty/null, and user.Credentials.FirstOrDefault would return null. Same behavior, so the IsNullOrEmpty check is unnecessary.

This is actually a regression from PR #3855, since some test infra uses empty API keys.

@chenriksson chenriksson merged commit 52dc307 into tempkeys May 8, 2017
@chenriksson chenriksson deleted the chenriks-tempkeys-onboard branch May 8, 2017 15:35
chenriksson added a commit that referenced this pull request May 16, 2017
chenriksson added a commit that referenced this pull request May 16, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants