-
Notifications
You must be signed in to change notification settings - Fork 644
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
Conversation
if (!result.Success) | ||
{ | ||
Auditing.SaveAuditRecordAsync(new FailedUserSecurityPolicyAuditRecord( | ||
user.Username, GetAuditAction(action), foundPolicies)).Wait(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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
/// <summary> | ||
/// Audit record for failed user security policy evaluations. | ||
/// </summary> | ||
public class FailedUserSecurityPolicyAuditRecord : AuditRecord<AuditedPackageAction> |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
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 |
🕐 |
@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) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why delete?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
🕐 |
Like was discussed with @joelverhagen and @dtivel , we should have auditing for successful policy validation as well. |
@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; } |
There was a problem hiding this comment.
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
.vs/config/applicationhost.config
Outdated
@@ -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" /> |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
?
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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>() |
There was a problem hiding this comment.
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.")); |
There was a problem hiding this comment.
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>(); |
There was a problem hiding this comment.
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?
Auditing and telemetry for user security policies (subscription, unsubscription, failed evaluations)