Skip to content

Add analyzer/codefixer to use AddAuthorizationBuilder#47428

Merged
captainsafia merged 29 commits into
dotnet:mainfrom
david-acker:use-add-authorization-builder-analyzer
May 9, 2023
Merged

Add analyzer/codefixer to use AddAuthorizationBuilder#47428
captainsafia merged 29 commits into
dotnet:mainfrom
david-acker:use-add-authorization-builder-analyzer

Conversation

@david-acker
Copy link
Copy Markdown
Member

Add UseAuthorizationBuilderAnalyzer, AddAuthorizationBuilderFixer

Adds an analyzer/codefixer to recommend that AddAuthorization be converted to AddAuthorizationBuilder.

Description

The code fix converts any usage of the setters for the DefaultPolicy, FaillbackPolicy, and InvokeHandlersAfterFailure properties on AuthorizationOptions to an equivalent method call (SetDefaultPolicy, SetFallbackPolicy, and SetInvokeHandlersAfterFailure) on AuthorizationBuilder.

No diagnostic is reported when the configure action passed to AddAuthorization uses the GetPolicy(String) method, the DefaultPolicy getter, the FallbackPolicy getter, or the InvokeHandlersAfterFailure getter on AuthorizationOptions as these do not exist on AuthorizationBuilder and thus cannot be converted.

No diagnostic is reported if the configure action passed to AddAuthorization contains operations unrelated to AuthorizationOptions, to avoid unintentionally deleting code when applying the code fix, as it would not be easy, or possible, to automatically map this to AddAuthorizationBuilder's fluent API.

Fixes #45219

@ghost ghost added area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer community-contribution Indicates that the PR has been added by a community member labels Mar 26, 2023
@ghost
Copy link
Copy Markdown

ghost commented Mar 26, 2023

Thanks for your PR, @david-acker. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@mkArtakMSFT
Copy link
Copy Markdown
Contributor

@halter73 can you please review this? Thanks!

Copy link
Copy Markdown
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

LGTM! Is there any particular reason this is still a draft? @captainsafia Can you take a look too?

}
}

private static bool CanReplaceWithAddAuthorizationBuilder(Diagnostic diagnostic, SyntaxNode root, [NotNullWhen(true)] out InvocationExpressionSyntax? invocation)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When would this return false after a diagnostic is emitted? If it's possible, should we avoid raising the diagnostic if we cannot do the fix?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Offhand, I don't believe there should be any situations where a diagnostic would be emitted and this would return false. The analyzer should account for all of the scenarios where we wouldn't be able to apply the code fix and not emit a diagnostic in those situations. But I'll take a closer look to make sure the analyzer isn't missing anything.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd just add an assert to document this if you're confident.

SyntaxFactory.Space,
SyntaxFactory.Space,
SyntaxFactory.Space,
SyntaxFactory.Space),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This part seems to be the sketchiest to me. I guess our tests expect SyntaxFactory.EndOfLine("\r\n"). Is this indentation relative to the previous line? Can we add tests for a more deeply nested call into AddAuthorization(...) like in HostBuilder.ConfigureServices?

Copy link
Copy Markdown
Member Author

@david-acker david-acker Mar 28, 2023

Choose a reason for hiding this comment

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

I agree. This part still felt pretty hacky to me, which is why I left the PR in draft. I haven't confirmed it yet, but I'm assuming this may also related to the test that's failing due to some line ending issues.

I added this in an attempt to improve the formatting of the code generated by the code fix so that it would match the example "after" code example in the issue, otherwise there wouldn't be any indentation relative to the previous line.

Should we be concerned with the fixed code being "pretty" (within reason) or should we leave this up to the user to handle? If we do want to handle formatting as part of the code fix, I can go ahead and add tests for more deeply nested scenarios. Otherwise, I'll remove this and update the fixedSource values in the tests.

Fixed example: without the added indentation and newline (from MultipleAddPolicyCalls_UsingExpressionBody_FixedWithAddAuthorizationBuilder)

builder.Services.AddAuthorizationBuilder().AddPolicy("AtLeast18", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18))).AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we want it to look reasonably pretty if we're going to do this. The example without the added indentation and newline does look bad.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How helpful is the SyntaxNode.NormalizeWhitespace API here for formatting the InvocationExpression? I know it sometimes doesn't play too nicely with deeply-nested calls but wondering if it might help here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

NormalizeWhitespace seemed to be removing new lines when I tried using it initially, which was causing some issues, but I'll do more testing to see if I can get it to work. Maybe calling NormalizeWhitespace and then manually adding a new line for each chained call would work?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Following up on this. Calling NormalizeWhitespace on the final InvocationExpression actually seems to format the block body examples OK, although the expression body examples ended up being pretty long since NormalizeWhitespace doesn't add new lines after the chained calls. So it looks like we'll need to add the EndOfLineTrivia ourselves if we want go this route.

Example: MultipleAddPolicyCalls_UsingBlockBody_FixedWithAddAuthorizationBuilder

builder.Services.AddAuthorizationBuilder().AddPolicy("AtLeast18", policy =>
{
    policy.Requirements.Add(new MinimumAgeRequirement(18));
}).AddPolicy("AtLeast21", policy =>
{
    policy.Requirements.Add(new MinimumAgeRequirement(21));
});

Example: MultipleAddPolicyCalls_UsingExpressionBody_FixedWithAddAuthorizationBuilder

builder.Services.AddAuthorizationBuilder().AddPolicy("AtLeast18", policy => policy.Requirements.Add(new MinimumAgeRequirement(18))).AddPolicy("AtLeast21", policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I also added two tests for the nested AddAuthorization scenarios and made some adjustments to have leading trivia is handled in AddAuthorizationBuilderFixer to ensure that indentation is correct.

@david-acker
Copy link
Copy Markdown
Member Author

@halter73 @captainsafia How should we handle the line-ending issues with the Linux and MacOS test failures? Should we just add OSSkipCondition to these tests like we did for the line-ending issue with Add analyzer and code fix to recommend against IHeaderDictionary.Add #44463?

@halter73
Copy link
Copy Markdown
Member

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@halter73
Copy link
Copy Markdown
Member

I didn't realize we're skipping tests due to line-ending issues. Ideally, we should split or normalize the line endings before asserting so developers can run the tests on any platform even if we don't expect platform-specific failures. We shouldn't assume everyone is developing on Windows.

We do this for our RequestDelegateGenerator tests.

var baseline = await File.ReadAllTextAsync(baselineFilePath);
var expectedLines = baseline
.TrimEnd() // Trim newlines added by autoformat
.Replace("%GENERATEDCODEATTRIBUTE%", RequestDelegateGeneratorSources.GeneratedCodeAttribute)
.Split(Environment.NewLine);
Assert.True(CompareLines(expectedLines, generatedCode, out var errorMessage), errorMessage);

@david-acker
Copy link
Copy Markdown
Member Author

david-acker commented Apr 24, 2023

How would you recommend hooking this into the current verification logic that's being used for these analyzer/code fix tests (from Microsoft.CodeAnalysis.Testing)? At first glance, it looks like it may be possible to add in the line-ending normalization logic if we were to override CodeFixTest.VerifyFixAsync, although the source text verifications take place down in some private methods so I'll have to do some more digging.

Also, as an aside, these tests did actually pass when I ran them locally on MacOS with a clean clone of this branch, oddly enough. I'm curious why this would be the case, since they seem to be failing consistently on CI.

@captainsafia
Copy link
Copy Markdown
Contributor

@david-acker This issue with line-endings originally came up in #45642. At the time, we implemented the ReplaceLineEndings workaround in a single test in anticipation that we could move it lower down into the verifier code if it was encountered in more tests (like you have here). See my comment here for where I think we should fix this.

@david-acker david-acker marked this pull request as ready for review April 30, 2023 20:19
Copy link
Copy Markdown
Contributor

@captainsafia captainsafia left a comment

Choose a reason for hiding this comment

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

Looking good! Thanks for the ReplaceLineEndings fixup. Left some comments and questions inline.

await VerifyCodeFix(source, new[] { diagnostic }, fixedSource);
}

private static async Task VerifyCodeFix(string source, DiagnosticResult[] diagnostics, string fixedSource)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we add a test for the scenario where the AddAuthorization call is assigned to a variable?

var services = builder.Services.AddAuthorization(options => ...);

I believe we will get the desired behavior here (not emitting a diagnostic) based on the IsLastCallInChain check but good to encode that behavior in tests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added AddAuthorization_CallAssignedToVariable_NoDiagnostic.


private static bool IsCompatibleWithAuthorizationBuilder(IInvocationOperation invocation, AuthorizationOptionsTypes authorizationOptionsTypes)
{
if (invocation is { Arguments: { Length: 2 } invocationArguments }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: the pattern checks here are very dense. Maybe factor these out into separate methods so it is clearer what we are asserting with each check?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've refactored each of these pattern checks into a separate private method to make it clearer what each one does. Right now I've replaced each check with it's respective method to keep things as "flat" as possible, although this does require these methods to effectively be chained with their out parameters.

Let me know if there are any additional improvements or changes you would suggest here!

{
if (operation is IExpressionStatementOperation { Operation: { } expressionStatementOperation })
{
if (expressionStatementOperation is ISimpleAssignmentOperation { Target: IPropertyReferenceOperation { Property.ContainingType: { } propertyReferenceContainingType } }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need add tests that what these code segments are checking (assuming I understood the code correctly) like:

builder.Services.AddAuthorization(options => ConfigureOptions(options));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've added some new tests and updated a few of the existing ones to ensure that all of the branches in these private methods are covered by tests. I also removed the MethodReferenceOperation-related branch as this was unreachable.


foreach (var diagnostic in context.Diagnostics)
{
if (CanReplaceWithAddAuthorizationBuilder(diagnostic, root, out var invocation))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yay! Thanks for doing the feasibility check before registering the codefix. This was an issue in the first few codefixers that we did...

@captainsafia captainsafia added the pr: pending author input For automation. Specifically separate from Needs: Author Feedback label May 2, 2023
Copy link
Copy Markdown
Contributor

@captainsafia captainsafia left a comment

Choose a reason for hiding this comment

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

LGTM! :shipit:

@captainsafia captainsafia merged commit 5d96254 into dotnet:main May 9, 2023
@ghost ghost added this to the 8.0-preview5 milestone May 9, 2023
@david-acker david-acker deleted the use-add-authorization-builder-analyzer branch May 9, 2023 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer community-contribution Indicates that the PR has been added by a community member pr: pending author input For automation. Specifically separate from Needs: Author Feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Analyzer/Codefixer] Recommend using AddAuthorizationBuilder for configuring global policies

4 participants