-
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 implementation (#3563) #3646
Conversation
src/NuGetGallery/App_Start/Routes.cs
Outdated
version = UrlParameter.Optional | ||
}); | ||
|
||
routes.MapRoute( |
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 add the endpoint here? Do we even document v1 push? I think the data should determine whether we add the endpoint here -- what does AppInsights suggest is the number of usages in the past 90 days?
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.
AI shows only v2 in use. My reasoning was to have same support as 'verifykey'. If we don't add it to v1 because of AI, why not just remove verifykey as well? Thoughts?
|
||
routes.MapRoute( | ||
"v2" + RouteName.CreatePackageVerificationKey, | ||
"api/v2/package/create-verification-key/{id}/{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.
No {version}
parameter, right?
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.
Correct - missed this one!
Update: per discussion, keeping version for telemetry
@@ -43,6 +46,8 @@ public partial class ApiController | |||
public IMessageService MessageService { get; set; } | |||
public IAuditingService AuditingService { get; set; } | |||
public IGalleryConfigurationService ConfigurationService { get; set; } | |||
public AuthenticationService AuthenticationService { get; set; } | |||
public ICredentialBuilder CredentialBuilder { get; set; } |
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.
nit: why do these properties have set;
? Typically in a DI world these are just set as ctor/instantiation time.
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.
Kept the existing pattern... will see if I can refactor here.
[ApiAuthorize] | ||
[ApiScopeRequired(NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion)] | ||
[ActionName("CreatePackageVerificationKey")] | ||
public virtual Task<ActionResult> CreatePackageVerificationKeyPutAsync(string id) |
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 support both PUT
and POST
? This is a new endpoint so we can be strict.
I actually thing POST
is more appropriate here since subsequent calls will create different API keys. (sorry if I have gone back and forth about this)
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 being consistent with package push, but agree we don't need both here. POST does seem the better option. @zhili1208 will need to update the client PR.
var routeDefaults = (RouteData.Route as Route)?.Defaults?.Values; | ||
var apiName = routeDefaults == null ? "Unknown" : string.Join("/", routeDefaults); | ||
|
||
HttpContext.Response.Headers.Add(Constants.WarningHeaderName, |
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 header is now very visible on the client. Every symbol push will result in this warning appearing. Do we really want this level of visibility? I am not sure.
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.
Removing this header
if (HttpContext.Response.Headers != null) | ||
{ | ||
var routeDefaults = (RouteData.Route as Route)?.Defaults?.Values; | ||
var apiName = routeDefaults == null ? "Unknown" : string.Join("/", routeDefaults); |
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.
When would this be "Unknown"? I think we should not show the warning of we can't tell them what's actually deprecated.
Could you give an example of what apiName
would be for the new create verify key endpoint?
return Json(new | ||
{ | ||
Key = credential.Value, | ||
Expires = credential.Expires.Value.ToString("O", CultureInfo.CurrentCulture), |
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.
We really want invariant culture here -- although am not sure if culture info even affects this.
https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
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 like "O" doesn't need culture. Thanks!
{ | ||
Key = credential.Value, | ||
Expires = credential.Expires.Value.ToString("O", CultureInfo.CurrentCulture), | ||
PackageScope = credential.Scopes.First().Subject |
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 return the scope here? If we are doing First()
it seems kind of arbitrary
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.
Scope is an implementation detail. We shouldn't expose it to the client.
// Expire and delete verification key after first use to avoid growing the database tables. | ||
if (isVerificationKey) | ||
{ | ||
await AuthenticationService.RemoveCredential(user, credential); |
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 may change the error flow for the client. Imagine we return HTTP 500 here due to an exception ApiKeyScopeAllows
. The key will be deleted in this finally
. If the 500 bubbles all the way back to the client (nuget.exe), when they will retry. To have the same user experience, the client MUST generate a new API key, right?
@zhili1208 -- is this the case? HTTP 500 in the client today causes the exact same HTTP request to be retried. In this particular scenario, we need to generate a new verification key before retrying (I don't recall this in your client PR).
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, 500 causes http request retry in client
{ | ||
Key = credential.Value, | ||
Expires = credential.Expires.Value.ToString("O", CultureInfo.CurrentCulture), | ||
PackageScope = credential.Scopes.First().Subject |
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.
Scope is an implementation detail. We shouldn't expose it to the client.
|
||
[HttpGet] | ||
[RequireSsl] | ||
[ApiAuthorize] | ||
[ApiScopeRequired(NuGetScopes.PackageVerify)] |
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.
you should add NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion to the allowed scopes
@@ -22,6 +22,7 @@ public static class ApiKey | |||
public const string Prefix = "apikey."; | |||
public const string V1 = Prefix + "v1"; | |||
public const string V2 = Prefix + "v2"; | |||
public const string V2Verify = Prefix + "verify.v2"; |
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.
IMO versioning of "verify api key" should be separate from the regular api keys. I recommend "verify.v1"
Some minor issues, otherwise looks great! |
5f4da31
to
d5d0c07
Compare
I don't think we should merge this to master until our clean-up job is running. This suggests we should have a feature flag to easily enable and disable creation of keys. What do you think? |
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 pretty good overall, just a couple things I would like to see changed
@@ -43,6 +46,8 @@ public partial class ApiController | |||
public IMessageService MessageService { get; set; } | |||
public IAuditingService AuditingService { get; set; } | |||
public IGalleryConfigurationService ConfigurationService { get; set; } | |||
public AuthenticationService AuthenticationService { get; set; } |
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 we not have an interface for AuthenticationService
? Seems weird when every single other property in this class uses an interface.
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.
No, we don't. :-)
@@ -50,6 +53,8 @@ public TestableApiController(MockBehavior behavior = MockBehavior.Default) | |||
StatisticsService = (MockStatisticsService = new Mock<IStatisticsService>()).Object; | |||
IndexingService = (MockIndexingService = new Mock<IIndexingService>()).Object; | |||
AutoCuratePackage = (MockAutoCuratePackage = new Mock<IAutomaticallyCuratePackageCommand>()).Object; | |||
CredentialBuilder = new CredentialBuilder(); |
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.
All of the other members of ApiController
are replaced with Mock
s here. The idea is that if you would like to use a functionality of an interface, you need to control it yourself by setting up the various methods to act how you want them to.
I would suggest removing AuthenticationService
from the constructor and using Mock
s like the other fields. You can then setup any methods you need.
@@ -36,12 +37,17 @@ public static Credential CreateV2ApiKey(Guid apiKey, TimeSpan? expiration) | |||
return CreateApiKey(CredentialTypes.ApiKey.V2, apiKey.ToString(), expiration); | |||
} | |||
|
|||
public static Credential CreateV2VerificationApiKey(Guid apiKey) |
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.
Side note about this file:
It's very strange that TestCredentialBuilder
is not an ICredentialBuilder
. I would suggest renaming it so that it's no longer confusing, for example TestCredentialHelper
.
{ | ||
// Arrange | ||
var controller = new TestableApiController(); | ||
controller.SetCurrentUser(new User()); | ||
var user = await CreateUserAsync(CredentialTypes.ApiKey.V2, "anysubject", allowedAction); |
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 an example of what I mean by the comment above, you can do the following to this test case to make it consistent with the testing pattern of the rest of the file.
- Change
CreateUserAsync
to not callAuthenticationService.AddCredential
and calluser.Credentials.Add(credential)
directly - Setup the
Mock<AuthenticationService>
to douser.Credentials.Add(credential)
whenAuthenticationService.AddCredential
is called - Verify that
AuthenticationService.AddCredential
was called (and check thatuser.Credential
is as expected)
An alternative to this is to change the TestableApiController
constructor to create the Mock<AuthenticationService>
with a setup of AuthenticationService.AddCredential
as mentioned in number 2 and then do number 3.
new Claim(NuGetClaims.ApiKey, string.Empty), | ||
new Claim(NuGetClaims.Scope, scopes)); | ||
apiKey = credential.Value; | ||
scopes = JsonConvert.SerializeObject(credential.Scopes, Formatting.None); |
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 doesn't seem right to me. We ask the user for scopes
, but if they have at least one API key already, then we ignore the requested scopes
and use the first API key instead.
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 think I'm unnecessarily overloading this method. Will look into separate overload so there's separate APIs for whether scopes are passed as strings or discovered from the user.
|
||
if (scopes != null) | ||
var credential = user.Credentials?.FirstOrDefault(c => c.Type.StartsWith(CredentialTypes.ApiKey.Prefix)); |
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 did we need to change this method?
{ | ||
var credential = new Credential( | ||
CredentialTypes.ApiKey.VerifyV1, | ||
Guid.NewGuid().ToString().ToLowerInvariant(), |
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.
Make a helper function for this. This logic is now in both CreateApiKey
and CreatePackageVerificationApiKey
. We should have a shared method for creating our keys.
private static string CreateKeyString()
{
return Guid.NewGuid().ToString().ToLowerInvariant();
}
[InlineData(CredentialTypes.ApiKey.VerifyV1, "notfoo", NuGetScopes.PackageVerify)] | ||
[InlineData(CredentialTypes.ApiKey.VerifyV1, "foo", NuGetScopes.PackagePush)] | ||
[InlineData(CredentialTypes.ApiKey.VerifyV1, "foo", NuGetScopes.PackagePushVersion)] | ||
public async void VerifyPackageKeyReturns403IfOwnerScopeDoesNotMatch(string credentialType, string subject, string allowedAction) |
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.
Since the behavior is not explicit here, I would add a comment explaining that VerifyPackageKey
will succeed when subject
is "foo" and scope
is NuGetScopes.PackageVerify
.
Also I think you should probably have a CredentialTypes.ApiKey.V2
and a CredentialTypes.ApiKey.VerifyV1
version for each test case if they act in the same way. In other words, there should be a CredentialTypes.ApiKey.VerifyV1
test for the "bar" case and a CredentialTypes.ApiKey.V2
case for the "foo" and "notfoo" cases, unless the behavior is different.
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.
Most of these test cases are covered - see below. Will try to make it more clear.
VerifyPackageKeyReturns403IfOwnerScopeDoesNotMatch - scopes don't match
- ApiKey.V2 where subject and action (Verify is only for VerifyV1) don't match
=> I should separate non-matching subject and action into separate test cases - ApiKey.VerifyV1 where subject doesn't match
- ApiKey.VerifyV1 where action doesn't match (push)
- ApiKey.VerifyV1 where action doesn't match (pushVersion)
VerifyPackageKeyReturns200IfUserIsAnOwner - scopes match
- ApiKey.V2 w/o scope (legacy)
- ApiKey.V2 with matching subject and action (Push)
- ApiKey.V2 with matching subject and action (PushVersion)
- ApiKey.VerifyV1 with matching subject and action (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.
Yeah, ultimately I think the main reason I found this confusing was that this test case tested both the subject and the action being incorrect. I think if you separate the test cases into two then it would make much more sense.
@scottbommarito Refactored the tests to better match existing patterns... both with mocking the auth service and string scopes declared in [InlineData] attributes. |
@joelverhagen Sounds reasonable. To avoid overloading this PR, how about I merge this and send PR for telemetry and feature flag tomorrow? |
As long as it makes it in before a merge to master: 👍 |
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 great!
98fc140
to
51b8cc9
Compare
…7.03.27 * tag 'v2017.03.27': (205 commits) Revert UpdateIsLatest optimistic concurrency changes (NuGet#3707) UpdateIsLatest concurrent unlist fix (NuGet#3695) Change telemetry time to use correct format (NuGet#3690) Fix typo of "publically" (NuGet#3636) Fix regression (NuGet#3667) Add credential to Register and RequestPasswordReset audits (NuGet#3666) Functional test for temp keys (NuGet#3664) Telemetry for temp keys (NuGet#3662) Temp keys implementation (NuGet#3563) (NuGet#3646) Extracting code: single type per file (NuGet#3644) Telemetry for package push (NuGet#3649) Upgrade to NuGet.* v4.0.0 dependencies (NuGet#3643) Fix concurrent push test by disabling search hijacking on feed (NuGet#3641) Fixing Package Description truncation (NuGet#3638) Fix Microsoft Account removal (NuGet#3639) Send e-mail when a new API key is created (NuGet#3634) IsLatest Fix: wrong connection string passed to retry context (NuGet#3632) Update WindowsAzure.Storage to 7.0.0 (NuGet#3633) Depend on signed version of Elmah (NuGet#3609) Move AzureEntityList and TableErrorLog to NuGetGallery.Core (NuGet#3607) ...
Telemetry will come in separate PR