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 telemetry and auditing #3945

Merged
merged 12 commits into from
May 16, 2017
Merged

Conversation

chenriksson
Copy link
Member

Auditing and telemetry for user security policies (subscription, unsubscription, failed evaluations)

if (!result.Success)
{
Auditing.SaveAuditRecordAsync(new FailedUserSecurityPolicyAuditRecord(
user.Username, GetAuditAction(action), foundPolicies)).Wait();
Copy link
Member Author

Choose a reason for hiding this comment

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

Want to call out this Wait() -- any better suggestions? The issue is that policy evaluation is done from an action filter, but MVC5 filters don't support async.

Alternatives, which I'm not fond of:
-Move policy evaluation inside action methods
-Fire SaveAuditRecordAsync, but don't wait
-Create the audit record here, but write from an async handler or something?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would recommend making Evaluate async, and moving the Wait() to the attribute. In this case, if Evaluate is called from a different location in the future (say, a controller), it can be called correctly


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

Copy link
Member

Choose a reason for hiding this comment

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

Yes agreed. Make as much of the stack async. If the filter is the thing that can't be async, that's the place that should have the .Result or .Wait().


In reply to: 116067013 [](ancestors = 116067013,116026021)

@@ -14,6 +16,16 @@ namespace NuGetGallery.Security
public interface ISecurityPolicyService
{
/// <summary>
/// Auditing for the security policy service.
/// </summary>
IAuditingService Auditing { get; }
Copy link
Member

Choose a reason for hiding this comment

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

Auditing [](start = 25, length = 8)

Commented offline about this, dependencies should not be on the interface

@joelverhagen
Copy link
Member

public enum AuditedUserAction

Is this enum serialized somewhere? If so, is it serialized as int or string?


Refers to: src/NuGetGallery.Core/Auditing/AuditedUserAction.cs:6 in 9358458. [](commit_id = 9358458, deletion_comment = False)

/// <summary>
/// Audit record for failed user security policy evaluations.
/// </summary>
public class FailedUserSecurityPolicyAuditRecord : AuditRecord<AuditedPackageAction>
Copy link
Member

Choose a reason for hiding this comment

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

FailedUserSecurityPolicyAuditRecord [](start = 17, length = 35)

I'm loving these read-only types. Very nice.

@@ -16,6 +17,7 @@ public class UserAuditRecord : AuditRecord<AuditedUserAction>
public CredentialAuditRecord[] Credentials { get; }
public CredentialAuditRecord[] AffectedCredential { get; }
public string AffectedEmailAddress { get; }
public AuditedUserSecurityPolicy[] Policies { get; }
Copy link
Member

Choose a reason for hiding this comment

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

AuditedUserSecurityPolicy [](start = 15, length = 25)

Nit, if it's all the same we should be using IEnumerable or IReadOnlyList if you need indexing. Arrays are mutable which is probably not necessary here.

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 Left as arrays, which is consistent with other audit records properties. I'd prefer to be consistent for now, and revisit as a separate item since it's general feedback for all auditing. Added this feedback to #3946.

Copy link
Member

Choose a reason for hiding this comment

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

Yup, sounds great.

@@ -12,7 +12,6 @@ public enum AuditedPackageAction
Unlist,
Edit,
UndoEdit,


Verify
Copy link
Contributor

Choose a reason for hiding this comment

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

Shims will need to be updated as well

@joelverhagen
Copy link
Member

We should wait till @dtivel fills us in the process of updating our internal auditing implementation. As it stands today, it seems like we should have both PRs ready and reviewed before we merge to dev.

@joelverhagen
Copy link
Member

🕐

@chenriksson
Copy link
Member Author

@joelverhagen action enum gets serialized as string in audit record. Also added audit record for Verify per our discussion.

This PR is against tempkeys feature branch, but agree I should have Shims PR ready before merging this to dev.

@@ -245,7 +245,7 @@ public virtual Task<ActionResult> GetNuGetExe()
return (ActionResult)result ?? new EmptyResult();
}

private HttpStatusCodeWithBodyResult VerifyPackageKeyInternal(User user, Credential credential, string id, string version)
private async Task<HttpStatusCodeWithBodyResult> VerifyPackageKeyInternal(User user, Credential credential, string id, string version)
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding "async" in the method name.

var evaluateTask = SecurityPolicyService.EvaluateAsync(SecurityPolicyAction.Value, httpContext);
evaluateTask.Wait();

SecurityPolicyResult = evaluateTask.Result;
Copy link
Contributor

Choose a reason for hiding this comment

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

.Result will do the wait. There is no need for the line 95.

CredentialTypes.IsApiKey(c.Type) &&
(
c.Scopes.Count == 0 ||
c.Scopes.Any(s =>
s.AllowedAction.Equals(NuGetScopes.PackagePush, StringComparison.OrdinalIgnoreCase) ||
s.AllowedAction.Equals(NuGetScopes.PackagePushVersion, StringComparison.OrdinalIgnoreCase)
))
);
).ToList();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was the ToList needed?


public override string GetPath()
{
return Path; // store in <auditpath>/failedusersecuritypolicy/all
Copy link
Contributor

Choose a reason for hiding this comment

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

does this path mean the blob storage folder where this audit record is stored? In that case why not save it in the user's folder?


namespace NuGetGallery.Auditing
{
public enum UserAuditAction
Copy link
Contributor

Choose a reason for hiding this comment

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

why delete?

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 dead code; UserAuditRecord uses AuditedUserAction instead.

Though, I should probably use present tense verbs for actions to match the others.

public UserAuditRecord(User user, AuditedUserAction action, IEnumerable<UserSecurityPolicy> policies)
: this(user, action, Enumerable.Empty<Credential>())
{
Policies = policies.Select(p => new AuditedUserSecurityPolicy(p)).ToArray();
Copy link
Contributor

Choose a reason for hiding this comment

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

policies are available as part of the user object. Why pass them separately?

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 should rename AffectedPolicies and add a comment. These only include the policies that are currently being subscribed to or unsubscribed from.

@skofman1
Copy link
Contributor

🕐

@skofman1
Copy link
Contributor

Like was discussed with @joelverhagen and @dtivel , we should have auditing for successful policy validation as well.

@chenriksson
Copy link
Member Author

@skofman1 @joelverhagen Added auditing of successful evaluation as well

@@ -18,6 +21,9 @@ public class SecurePushSubscription : IUserSecurityPolicySubscription
private const string MinClientVersion = "4.1.0";
private const int PushKeysExpirationInDays = 30;

private IAuditingService Auditing { get; }
Copy link
Member

Choose a reason for hiding this comment

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

IAuditingService [](start = 16, length = 16)

nit: typically these are private readonly fields rather than props

@@ -155,7 +155,7 @@
<sites>
<site name="NuGet Gallery (nuget.localtest.me)" id="2">
<application path="/">
<virtualDirectory path="/" physicalPath="..\..\src\NuGetGallery" />
<virtualDirectory path="/" physicalPath="D:\Nuget\NuGetGallery\src\NuGetGallery" />
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this file be checked in? This path doesn't exist on my machine.

Perhaps add this file to .gitignore.

}
if (policies == null || policies.Count() == 0)
{
throw new ArgumentException(nameof(policies));
Copy link
Contributor

Choose a reason for hiding this comment

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

This constructor takes a message. This other constructor takes a message and parameter name.

/// <summary>
/// Audit record for failed user security policy evaluations.
/// </summary>
public class FailedUserSecurityPolicyAuditRecord : AuditRecord<AuditedSecurityPolicyAction>
Copy link
Contributor

Choose a reason for hiding this comment

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

FailedSecurityPolicyEvaluationAuditRecord or FailedUserSecurityPolicyEvaluationAuditRecord?

/// <summary>
/// Subset of user policies affected by the action (subscription / unsubscription).
/// </summary>
public AuditedUserSecurityPolicy[] AffectedPolicies { get; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Be sure to update shims.

@@ -39,6 +39,6 @@ public interface ISecurityPolicyService
/// <param name="action">Security policy action.</param>
/// <param name="context">Authorization context.</param>
/// <returns>Policy result indicating success or failure.</returns>
SecurityPolicyResult Evaluate(SecurityPolicyAction action, HttpContextBase httpContext);
Task<SecurityPolicyResult> EvaluateAsync(SecurityPolicyAction action, HttpContextBase httpContext);
Copy link
Contributor

Choose a reason for hiding this comment

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

    /// <returns>A task that represents the asynchronous operation.
    /// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="SecurityPolicyResult" />
    /// instance.</returns>

@@ -56,73 +58,92 @@ public void UserHandlers()
}

[Fact]
public void EvaluateThrowsIfHttpContextNull()
public async void EvaluateThrowsIfHttpContextNull()
Copy link
Contributor

Choose a reason for hiding this comment

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

As a general rule I like to see test names in the format:

MemberName_Expectation

For example, this test would be:

EvaluateAsync_ThrowsForNullHttpContext

Then it is very clear what is being tested and what the expectation is. Without this and with refactorings over time you can be left with tests where the original intent isn't clear.

The previous test (UserHandlers) doesn't follow this format, so the intent of the test isn't stated. Other tests (like this one) don't seem to accurately describe the member under test. It may, but from a cursory look I didn't see OnSubscribe anywhere in the test. Shouldn't it be SubscribeAsync_DoesNotExpireNonPushCredentials?

Can you please review all your test names to ensure they are both accurate and clearly named? Most of them look pretty good with respect to stating expectations, but the name of the member under test isn't always accurate and some expectations are just missing.


namespace NuGetGallery.Auditing
{
public class UserSecurityPolicyAuditRecordFacts
Copy link
Contributor

Choose a reason for hiding this comment

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

Need more tests than just null/empty argument validation. What about verifying that the constructor initializes properties correctly? There is non-trivial code in the constructor that is testable.

What about testing the GetPath(...) override?

Copy link
Member Author

Choose a reason for hiding this comment

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

added

return SecurityPolicyResult.SuccessResult;
}

private AuditedSecurityPolicyAction GetAuditAction(SecurityPolicyAction policyAction)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have EvaluateAsync(...) tests that cover each case below?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes - reviewed

@@ -55,7 +61,13 @@ public UserAuditRecord(User user, AuditedUserAction action, string affectedEmail
{
AffectedEmailAddress = affectedEmailAddress;
}


public UserAuditRecord(User user, AuditedUserAction action, IEnumerable<UserSecurityPolicy> affectedPolicies)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add tests for this constructor in UserAuditRecordTests.cs.

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

@@ -17,15 +19,18 @@ public class SecurePushSubscriptionFacts
[Fact]
public void SubscriptionName()
{
// Arrange.
var subscription = CreateSecurityPolicyService().UserSubscriptions.First();
Copy link
Contributor

Choose a reason for hiding this comment

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

Would Single() be a stronger test? The same feedback applies to subsequent tests.

if (!key.Expires.HasValue || key.Expires > expires)
{
await _auditing.SaveAuditRecordAsync(
Copy link
Contributor

@loic-sharma loic-sharma May 12, 2017

Choose a reason for hiding this comment

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

From my understanding, this will save each audit record sequentially. Would it be better to kick off all of the save audit record tasks and then doing an await Task.WhenAll?

Copy link
Contributor

@dtivel dtivel left a comment

Choose a reason for hiding this comment

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

Looks good. Thanks, Christy!

@@ -88,7 +88,8 @@ public virtual IEnumerable<IUserSecurityPolicySubscription> UserSubscriptions
var result = handler.Evaluate(new UserSecurityPolicyEvaluationContext(httpContext, foundPolicies));

await Auditing.SaveAuditRecordAsync(new UserSecurityPolicyAuditRecord(
user.Username, GetAuditAction(action), foundPolicies, result.Success, result.ErrorMessage));
user.Username, GetAuditAction(action), foundPolicies, result.Success, result.ErrorMessage))
.ConfigureAwait(false);
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 explain this? From my understanding, this means you don't need to save the current context, but aren't you using the context's result directly after this await?

Copy link
Member Author

@chenriksson chenriksson May 15, 2017

Choose a reason for hiding this comment

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

Mistake... removed. Had accidentally left it in after trying something...

namespace NuGetGallery.Auditing
{
/// <summary>
/// Audit record for failed user security policy evaluations.
Copy link
Contributor

Choose a reason for hiding this comment

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

failed and succeeded policies evaluation

@@ -207,6 +207,10 @@ protected override void Load(ContainerBuilder builder)
.As<ISecurityPolicyService>()
.InstancePerLifetimeScope();

builder.RegisterType<SecurePushSubscription>()
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason not to have it as a SingleInstance() ? Same for SecurityPolicyService. The more we use singleton, the cheaper a request will be.


// Write an audit record
await AuditingService.SaveAuditRecordAsync(
new PackageAuditRecord(package, AuditedPackageAction.Verify, "Verified via API."));
Copy link
Contributor

Choose a reason for hiding this comment

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

is the string "Verified via API." needed? The action implies that already. Consider removing.

@@ -182,7 +233,7 @@ private static IEnumerable<UserSecurityPolicyHandler> CreateUserHandlers()
/// </summary>
private static IEnumerable<IUserSecurityPolicySubscription> CreateUserSubscriptions()
{
yield return new SecurePushSubscription();
yield return DependencyResolver.Current.GetService<SecurePushSubscription>();
Copy link
Contributor

@skofman1 skofman1 May 16, 2017

Choose a reason for hiding this comment

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

why not use dependency injection in the ctor?

@chenriksson chenriksson merged this pull request into tempkeys May 16, 2017
@chenriksson chenriksson deleted the chenriks-tempkeys-auditing branch May 16, 2017 18:34
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.

7 participants