diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Authorization/AddAuthorizationBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Authorization/AddAuthorizationBuilderAnalyzer.cs new file mode 100644 index 000000000000..957d8eb8345f --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Authorization/AddAuthorizationBuilderAnalyzer.cs @@ -0,0 +1,234 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Analyzers.Authorization; + +using WellKnownType = WellKnownTypeData.WellKnownType; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class AddAuthorizationBuilderAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseAddAuthorizationBuilder); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + var wellKnownTypes = WellKnownTypes.GetOrCreate(context.Compilation); + + var authorizationOptionsTypes = new AuthorizationOptionsTypes(wellKnownTypes); + if (!authorizationOptionsTypes.HasRequiredTypes) + { + return; + } + + var policyServiceCollectionExtensions = wellKnownTypes.Get(WellKnownType.Microsoft_Extensions_DependencyInjection_PolicyServiceCollectionExtensions); + if (policyServiceCollectionExtensions is null) + { + return; + } + + var addAuthorizationMethod = policyServiceCollectionExtensions.GetMembers() + .OfType() + .FirstOrDefault(member => member is { Name: "AddAuthorization", Parameters.Length: 2 }); + + if (addAuthorizationMethod is null) + { + return; + } + + context.RegisterOperationAction(context => + { + var invocation = (IInvocationOperation)context.Operation; + + if (SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, addAuthorizationMethod) + && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, policyServiceCollectionExtensions) + && IsLastCallInChain(invocation) + && IsCompatibleWithAuthorizationBuilder(invocation, authorizationOptionsTypes)) + { + AddDiagnosticInformation(context, invocation.Syntax.GetLocation()); + } + + }, OperationKind.Invocation); + } + + private static bool IsCompatibleWithAuthorizationBuilder(IInvocationOperation invocation, AuthorizationOptionsTypes authorizationOptionsTypes) + { + if (TryGetConfigureArgumentOperation(invocation, out var configureArgumentOperation) + && TryGetConfigureDelegateCreationOperation(configureArgumentOperation, out var configureDelegateCreationOperation) + && TryGetConfigureAnonymousFunctionOperation(configureDelegateCreationOperation, out var configureAnonymousFunctionOperation) + && TryGetConfigureBlockOperation(configureAnonymousFunctionOperation, out var configureBlockOperation)) + { + // Ensure that the child operations of the configuration action passed to AddAuthorization are all related to AuthorizationOptions. + var allOperationsInvolveAuthorizationOptions = configureBlockOperation.ChildOperations + .Where(operation => operation is not IReturnOperation { IsImplicit: true }) + .All(operation => DoesOperationInvolveAuthorizationOptions(operation, authorizationOptionsTypes)); + + return allOperationsInvolveAuthorizationOptions + // Ensure that the configuration action passed to AddAuthorization does not use any AuthorizationOptions-specific APIs. + && IsConfigureActionCompatibleWithAuthorizationBuilder(configureBlockOperation, authorizationOptionsTypes); + } + + return false; + } + + private static bool TryGetConfigureArgumentOperation(IInvocationOperation invocation, [NotNullWhen(true)] out IArgumentOperation? configureArgumentOperation) + { + configureArgumentOperation = null; + + if (invocation is { Arguments: { Length: 2 } invocationArguments }) + { + configureArgumentOperation = invocationArguments[1]; + return true; + } + + return false; + } + + private static bool TryGetConfigureDelegateCreationOperation(IArgumentOperation configureArgumentOperation, [NotNullWhen(true)] out IDelegateCreationOperation? configureDelegateCreationOperation) + { + configureDelegateCreationOperation = null; + + if (configureArgumentOperation is { ChildOperations: { Count: 1 } argumentChildOperations } + && argumentChildOperations.First() is IDelegateCreationOperation delegateCreationOperation) + { + configureDelegateCreationOperation = delegateCreationOperation; + return true; + } + + return false; + } + + private static bool TryGetConfigureAnonymousFunctionOperation(IDelegateCreationOperation configureDelegateCreationOperation, [NotNullWhen(true)] out IAnonymousFunctionOperation? configureAnonymousFunctionOperation) + { + configureAnonymousFunctionOperation = null; + + if (configureDelegateCreationOperation is { ChildOperations: { Count: 1 } delegateCreationChildOperations } + && delegateCreationChildOperations.First() is IAnonymousFunctionOperation anonymousFunctionOperation) + { + configureAnonymousFunctionOperation = anonymousFunctionOperation; + return true; + } + + return false; + } + + private static bool TryGetConfigureBlockOperation(IAnonymousFunctionOperation configureAnonymousFunctionOperation, [NotNullWhen(true)] out IBlockOperation? configureBlockOperation) + { + configureBlockOperation = null; + + if (configureAnonymousFunctionOperation is { ChildOperations: { Count: 1 } anonymousFunctionChildOperations } + && anonymousFunctionChildOperations.First() is IBlockOperation blockOperation) + { + configureBlockOperation = blockOperation; + return true; + } + + return false; + } + + private static bool DoesOperationInvolveAuthorizationOptions(IOperation operation, AuthorizationOptionsTypes authorizationOptionsTypes) + { + if (operation is IExpressionStatementOperation { Operation: { } expressionStatementOperation }) + { + if (expressionStatementOperation is ISimpleAssignmentOperation { Target: IPropertyReferenceOperation { Property.ContainingType: { } propertyReferenceContainingType } } + && SymbolEqualityComparer.Default.Equals(propertyReferenceContainingType, authorizationOptionsTypes.AuthorizationOptions)) + { + return true; + } + + if (expressionStatementOperation is IInvocationOperation { TargetMethod.ContainingType: { } invokedMethodContainingType } + && SymbolEqualityComparer.Default.Equals(invokedMethodContainingType, authorizationOptionsTypes.AuthorizationOptions)) + { + return true; + } + } + + return false; + } + + private static bool IsConfigureActionCompatibleWithAuthorizationBuilder(IBlockOperation configureAction, AuthorizationOptionsTypes authorizationOptionsTypes) + { + var usesAuthorizationOptionsSpecificAPIs = configureAction.Descendants() + .Any(operation => UsesAuthorizationOptionsSpecificGetters(operation, authorizationOptionsTypes) + || UsesAuthorizationOptionsGetPolicy(operation, authorizationOptionsTypes)); + + return !usesAuthorizationOptionsSpecificAPIs; + } + + private static bool UsesAuthorizationOptionsSpecificGetters(IOperation operation, AuthorizationOptionsTypes authorizationOptionsTypes) + { + if (operation is IPropertyReferenceOperation propertyReferenceOperation) + { + var property = propertyReferenceOperation.Property; + + // Check that the referenced property is not being set. + if (propertyReferenceOperation.Parent is IAssignmentOperation { Target: IPropertyReferenceOperation targetProperty } + && SymbolEqualityComparer.Default.Equals(property, targetProperty.Property)) + { + // Ensure the referenced property isn't being assigned to itself + // (i.e. options.DefaultPolicy = options.DefaultPolicy;) + if (propertyReferenceOperation.Parent is IAssignmentOperation { Value: IPropertyReferenceOperation valueProperty } + && SymbolEqualityComparer.Default.Equals(property, valueProperty.Property)) + { + return true; + } + + return false; + } + + if (SymbolEqualityComparer.Default.Equals(property, authorizationOptionsTypes.DefaultPolicy) + || SymbolEqualityComparer.Default.Equals(property, authorizationOptionsTypes.FallbackPolicy) + || SymbolEqualityComparer.Default.Equals(property, authorizationOptionsTypes.InvokeHandlersAfterFailure)) + { + return true; + } + } + + return false; + } + + private static bool UsesAuthorizationOptionsGetPolicy(IOperation operation, AuthorizationOptionsTypes authorizationOptionsTypes) + { + if (operation is IMethodReferenceOperation methodReferenceOperation + && SymbolEqualityComparer.Default.Equals(methodReferenceOperation.Member, authorizationOptionsTypes.GetPolicy) + && SymbolEqualityComparer.Default.Equals(methodReferenceOperation.Member.ContainingType, authorizationOptionsTypes.AuthorizationOptions)) + { + return true; + } + + if (operation is IInvocationOperation invocationOperation + && SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod, authorizationOptionsTypes.GetPolicy) + && SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod.ContainingType, authorizationOptionsTypes.AuthorizationOptions)) + { + return true; + } + + return false; + } + + private static bool IsLastCallInChain(IInvocationOperation invocation) + { + return invocation.Parent is IExpressionStatementOperation; + } + + private static void AddDiagnosticInformation(OperationAnalysisContext context, Location location) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UseAddAuthorizationBuilder, + location)); + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Authorization/AuthorizationOptionsTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Authorization/AuthorizationOptionsTypes.cs new file mode 100644 index 000000000000..ceada6264288 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Authorization/AuthorizationOptionsTypes.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Analyzers.Authorization; + +using WellKnownType = WellKnownTypeData.WellKnownType; + +internal sealed class AuthorizationOptionsTypes +{ + public AuthorizationOptionsTypes(WellKnownTypes wellKnownTypes) + { + AuthorizationOptions = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Authorization_AuthorizationOptions); + + if (AuthorizationOptions is not null) + { + var authorizationOptionsMembers = AuthorizationOptions.GetMembers(); + + var authorizationOptionsProperties = authorizationOptionsMembers.OfType(); + + DefaultPolicy = authorizationOptionsProperties + .FirstOrDefault(member => member.Name == "DefaultPolicy"); + FallbackPolicy = authorizationOptionsProperties + .FirstOrDefault(member => member.Name == "FallbackPolicy"); + InvokeHandlersAfterFailure = authorizationOptionsProperties + .FirstOrDefault(member => member.Name == "InvokeHandlersAfterFailure"); + + GetPolicy = authorizationOptionsMembers.OfType() + .FirstOrDefault(member => member.Name == "GetPolicy"); + } + } + + public INamedTypeSymbol? AuthorizationOptions { get; } + public IPropertySymbol? DefaultPolicy { get; } + public IPropertySymbol? FallbackPolicy { get; } + public IPropertySymbol? InvokeHandlersAfterFailure { get; } + public IMethodSymbol? GetPolicy { get; } + + public bool HasRequiredTypes => AuthorizationOptions is not null + && DefaultPolicy is not null + && FallbackPolicy is not null + && InvokeHandlersAfterFailure is not null + && GetPolicy is not null; +} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs index daba6a423cd7..85bc320937b6 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs @@ -205,4 +205,13 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Error, isEnabledByDefault: true, helpLinkUri: "https://aka.ms/aspnet/analyzers"); + + internal static readonly DiagnosticDescriptor UseAddAuthorizationBuilder = new( + "ASP0025", + new LocalizableResourceString(nameof(Resources.Analyzer_UseAddAuthorizationBuilder_Title), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.Analyzer_UseAddAuthorizationBuilder_Message), Resources.ResourceManager, typeof(Resources)), + "Usage", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + helpLinkUri: "https://aka.ms/aspnet/analyzers"); } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx index 2054f3b58106..f3a1901a5ff4 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx @@ -309,4 +309,10 @@ Route '{0}' conflicts with another action route. An HTTP request that matches multiple routes results in an ambiguous match error. Fix the conflict by changing the route's pattern, HTTP method, or route constraints. - + + Use AddAuthorizationBuilder to register authorization services and construct policies. + + + Use AddAuthorizationBuilder + + \ No newline at end of file diff --git a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Authorization/AddAuthorizationBuilderFixer.cs b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Authorization/AddAuthorizationBuilderFixer.cs new file mode 100644 index 000000000000..78b60966bb58 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Authorization/AddAuthorizationBuilderFixer.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.AspNetCore.Analyzers.Authorization.Fixers; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class AddAuthorizationBuilderFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.UseAddAuthorizationBuilder.Id); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) + { + return; + } + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel == null) + { + return; + } + + foreach (var diagnostic in context.Diagnostics) + { + if (CanReplaceWithAddAuthorizationBuilder(diagnostic, root, out var invocation)) + { + const string title = "Use 'AddAuthorizationBuilder'"; + context.RegisterCodeFix( + CodeAction.Create(title, + cancellationToken => ReplaceWithAddAuthorizationBuilder(diagnostic, root, context.Document, invocation), + equivalenceKey: DiagnosticDescriptors.UseAddAuthorizationBuilder.Id), + diagnostic); + } + } + } + + private static bool CanReplaceWithAddAuthorizationBuilder(Diagnostic diagnostic, SyntaxNode root, [NotNullWhen(true)] out InvocationExpressionSyntax? invocation) + { + invocation = null; + + var diagnosticTarget = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + if (diagnosticTarget is InvocationExpressionSyntax { ArgumentList.Arguments: { Count: 1 } arguments, Expression: MemberAccessExpressionSyntax { Name.Identifier: { } identifierToken } memberAccessExpression } + && arguments[0].Expression is SimpleLambdaExpressionSyntax lambda) + { + IEnumerable nodes; + + if (lambda.Body is BlockSyntax lambdaBlockBody) + { + nodes = lambdaBlockBody.DescendantNodes(); + } + else if (lambda.Body is InvocationExpressionSyntax lambdaExpressionBody) + { + nodes = new[] { lambdaExpressionBody }; + } + else + { + Debug.Assert(false, "AddAuthorizationBuilderAnalyzer should not have emitted a diagnostic."); + return false; + } + + var addAuthorizationBuilderMethod = memberAccessExpression.ReplaceToken(identifierToken, + SyntaxFactory.Identifier("AddAuthorizationBuilder")); + + invocation = SyntaxFactory.InvocationExpression(addAuthorizationBuilderMethod); + + foreach (var configureAction in nodes) + { + if (configureAction is InvocationExpressionSyntax { ArgumentList.Arguments: { Count: 2 } configureArguments, Expression: MemberAccessExpressionSyntax { Name.Identifier.Text: "AddPolicy" } }) + { + invocation = ChainInvocation( + invocation, + "AddPolicy", + SyntaxFactory.ArgumentList( + SyntaxFactory.SeparatedList(configureArguments))); + } + else if (configureAction is AssignmentExpressionSyntax { Left: MemberAccessExpressionSyntax { Name.Identifier.Text: { } assignmentTargetName }, Right: { } assignmentExpression } + && assignmentTargetName is "DefaultPolicy" or "FallbackPolicy" or "InvokeHandlersAfterFailure") + { + invocation = ChainInvocation( + invocation, + $"Set{assignmentTargetName}", + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(assignmentExpression)))); + } + } + + return true; + } + + Debug.Assert(false, "AddAuthorizationBuilderAnalyzer should not have emitted a diagnostic."); + return false; + } + + private static InvocationExpressionSyntax ChainInvocation( + InvocationExpressionSyntax invocation, + string invokedMemberName, + ArgumentListSyntax argumentList) + { + var invocationLeadingTrivia = invocation.GetLeadingTrivia() + .Where(trivia => !trivia.IsKind(SyntaxKind.EndOfLineTrivia)); + var newInvocationTrivia = new SyntaxTriviaList( + SyntaxFactory.EndOfLine(Environment.NewLine), + SyntaxFactory.Tab) + .AddRange(invocationLeadingTrivia); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + invocation.WithTrailingTrivia(newInvocationTrivia), + SyntaxFactory.IdentifierName(invokedMemberName)), + argumentList); + } + + private static Task ReplaceWithAddAuthorizationBuilder(Diagnostic diagnostic, SyntaxNode root, Document document, InvocationExpressionSyntax invocation) + { + var diagnosticTarget = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + return Task.FromResult(document.WithSyntaxRoot( + root.ReplaceNode(diagnosticTarget, invocation))); + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/Authorization/AddAuthorizationBuilderTests.cs b/src/Framework/AspNetCoreAnalyzers/test/Authorization/AddAuthorizationBuilderTests.cs new file mode 100644 index 000000000000..6d56ec02991c --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/Authorization/AddAuthorizationBuilderTests.cs @@ -0,0 +1,750 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis.Testing; +using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpCodeFixVerifier< + Microsoft.AspNetCore.Analyzers.Authorization.AddAuthorizationBuilderAnalyzer, + Microsoft.AspNetCore.Analyzers.Authorization.Fixers.AddAuthorizationBuilderFixer>; + +namespace Microsoft.AspNetCore.Analyzers.Authorization; + +public sealed class AddAuthorizationBuilderTests +{ + [Fact] + public async Task ConfigureAction_UsingExpressionBody_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(configure: options => + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))))|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task SingleAddPolicyCall_UsingExpressionBody_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(options => +{ + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +})|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task MultipleAddPolicyCalls_UsingExpressionBody_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(options => +{ + options.AddPolicy(""AtLeast18"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(18))); + + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +})|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .AddPolicy(""AtLeast18"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(18))) + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task SingleAddPolicyCall_UsingBlockBody_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(options => +{ + options.AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); +})|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task MultipleAddPolicyCalls_UsingBlockBody_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(options => +{ + options.AddPolicy(""AtLeast18"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(18)); + }); + + options.AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); +})|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .AddPolicy(""AtLeast18"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(18)); + }) + .AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task AuthorizationOptions_DefaultPolicyAssignment_ReplacedWithSetDefaultPolicyInvocation() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(options => +{ + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .RequireClaim(""Claim"") + .Build(); + + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +})|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .SetDefaultPolicy(new AuthorizationPolicyBuilder() + .RequireClaim(""Claim"") + .Build()) + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task AuthorizationOptions_FallbackPolicyAssignment_ReplacedWithSetFallbackPolicyInvocation() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(options => +{ + options.FallbackPolicy = new AuthorizationPolicyBuilder() + .RequireClaim(""Claim"") + .Build(); + + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +})|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .SetFallbackPolicy(new AuthorizationPolicyBuilder() + .RequireClaim(""Claim"") + .Build()) + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task AuthorizationOptions_InvokeHandlersAfterFailureAssignment_ReplacedWithSetInvokeHandlersAfterFailureInvocation() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddAuthorization(options => +{ + options.InvokeHandlersAfterFailure = false; + + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +})|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorizationBuilder() + .SetInvokeHandlersAfterFailure(false) + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task AddAuthorization_IsTheLastCallInChain_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +{|#0:builder.Services.AddRouting() + .AddAuthorization(options => + { + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); + })|}; +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRouting() + .AddAuthorizationBuilder() + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task AddAuthorization_IsNotTheLastCallInChain_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); +}) +.AddAuthentication(); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_DefaultPolicyAccess_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.FallbackPolicy = options.DefaultPolicy; +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_DefaultPolicyAccess_SelfAssignment_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.DefaultPolicy = options.DefaultPolicy; +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_FallbackPolicyAccess_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.DefaultPolicy = options.FallbackPolicy; +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_FallbackPolicyAccess_SelfAssignment_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.FallbackPolicy = options.FallbackPolicy; +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_InvokeHandlesAfterFailureAccess_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.InvokeHandlersAfterFailure = !options.InvokeHandlersAfterFailure; +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_InvokeHandlesAfterFailureAccess_SelfAssignment_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.InvokeHandlersAfterFailure = options.InvokeHandlersAfterFailure; +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_GetPolicyReference_NoDiagnostic() + { + var source = @" +using System; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy(""AtLeast21"", ((Func)options.GetPolicy)(string.Empty)!); +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task AuthorizationOptions_GetPolicyInvocation_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy(""AtLeast21"", options.GetPolicy(string.Empty)!); +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task ConfigureAction_IsNotAnonymousFunction_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(Helper.ConfigureAuthorization); + +public static class Helper +{ + public static void ConfigureAuthorization(AuthorizationOptions options) + { + options.AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); + } +} +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task ConfigureAction_AuthorizationOptionsPassedToMethodCall_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => Helper.ConfigureAuthorization(options)); + +public static class Helper +{ + public static void ConfigureAuthorization(AuthorizationOptions options) + { + options.AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); + } +} +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task ConfigureAction_ContainsOperationsNotRelatedToAuthorizationOptions_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(options => +{ + var value = 1 + 1; + options.AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); +}); +"; + + await VerifyNoCodeFix(source); + } + + [Fact] + public async Task NestedAddAuthorization_UsingBlockBody_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = new HostBuilder() + .ConfigureServices((context, services) => + { + {|#0:services.AddAuthorization(options => + { + options.AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); + })|}; + }); +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = new HostBuilder() + .ConfigureServices((context, services) => + { + services.AddAuthorizationBuilder() + .AddPolicy(""AtLeast21"", policy => + { + policy.Requirements.Add(new MinimumAgeRequirement(21)); + }); + }); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task NestedAddAuthorization_UsingExpressionBody_FixedWithAddAuthorizationBuilder() + { + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseAddAuthorizationBuilder) + .WithLocation(0) + .WithMessage(Resources.Analyzer_UseAddAuthorizationBuilder_Message); + + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = new HostBuilder() + .ConfigureServices((context, services) => + { + {|#0:services.AddAuthorization(options => + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))))|}; + }); +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = new HostBuilder() + .ConfigureServices((context, services) => + { + services.AddAuthorizationBuilder() + .AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21))); + }); +"; + + await VerifyCodeFix(source, new[] { diagnostic }, fixedSource); + } + + [Fact] + public async Task AddAuthorization_CallAssignedToVariable_NoDiagnostic() + { + var source = @" +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +var services = builder.Services.AddAuthorization(options => + options.AddPolicy(""AtLeast21"", policy => + policy.Requirements.Add(new MinimumAgeRequirement(21)))); +"; + + await VerifyNoCodeFix(source); + } + + private static async Task VerifyCodeFix(string source, DiagnosticResult[] diagnostics, string fixedSource) + { + var fullSource = string.Join(Environment.NewLine, source, _testAuthorizationPolicyClassDeclaration); + var fullFixedSource = string.Join(Environment.NewLine, fixedSource, _testAuthorizationPolicyClassDeclaration); + + await VerifyCS.VerifyCodeFixAsync(fullSource, diagnostics, fullFixedSource); + } + + private static async Task VerifyNoCodeFix(string source) + { + var fullSource = string.Join(Environment.NewLine, source, _testAuthorizationPolicyClassDeclaration); + + await VerifyCS.VerifyCodeFixAsync(fullSource, Array.Empty(), fullSource); + } + + private const string _testAuthorizationPolicyClassDeclaration = @" +public class MinimumAgeRequirement: IAuthorizationRequirement +{ + public int Age { get; } + public MinimumAgeRequirement(int age) => Age = age; +} +"; +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpAnalyzerVerifier.cs b/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpAnalyzerVerifier.cs index db13c60dbe9c..623ad13e3ab1 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpAnalyzerVerifier.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpAnalyzerVerifier.cs @@ -34,7 +34,7 @@ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticRes { var test = new CSharpAnalyzerTest { - TestCode = source, + TestCode = source.ReplaceLineEndings(), // We need to set the output type to an exe to properly // support top-level programs in the tests. Otherwise, // the test infra will assume we are trying to build a library. diff --git a/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpCodeFixVerifier.cs b/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpCodeFixVerifier.cs index 124b40083885..d3e39d009851 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpCodeFixVerifier.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpCodeFixVerifier.cs @@ -31,7 +31,7 @@ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticRes { var test = new CSharpCodeFixTest { - TestCode = source, + TestCode = source.ReplaceLineEndings(), // We need to set the output type to an exe to properly // support top-level programs in the tests. Otherwise, // the test infra will assume we are trying to build a library. @@ -59,8 +59,8 @@ public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] ex // We need to set the output type to an exe to properly // support top-level programs in the tests. Otherwise, // the test infra will assume we are trying to build a library. - TestState = { Sources = { source }, OutputKind = OutputKind.ConsoleApplication }, - FixedState = { Sources = { fixedSource } }, + TestState = { Sources = { source.ReplaceLineEndings() }, OutputKind = OutputKind.ConsoleApplication }, + FixedState = { Sources = { fixedSource.ReplaceLineEndings() } }, ReferenceAssemblies = CSharpAnalyzerVerifier.GetReferenceAssemblies(), NumberOfFixAllIterations = expectedIterations, CodeActionEquivalenceKey = codeActionEquivalenceKey diff --git a/src/Shared/RoslynUtils/WellKnownTypeData.cs b/src/Shared/RoslynUtils/WellKnownTypeData.cs index fd615a73d828..33170c0d1b23 100644 --- a/src/Shared/RoslynUtils/WellKnownTypeData.cs +++ b/src/Shared/RoslynUtils/WellKnownTypeData.cs @@ -109,7 +109,9 @@ public enum WellKnownType Microsoft_AspNetCore_Mvc_ValidateAntiForgeryTokenAttribute, Microsoft_AspNetCore_Mvc_ModelBinding_EmptyBodyBehavior, Microsoft_AspNetCore_Authorization_AllowAnonymousAttribute, - Microsoft_AspNetCore_Authorization_AuthorizeAttribute + Microsoft_AspNetCore_Authorization_AuthorizeAttribute, + Microsoft_Extensions_DependencyInjection_PolicyServiceCollectionExtensions, + Microsoft_AspNetCore_Authorization_AuthorizationOptions } public static string[] WellKnownTypeNames = new[] @@ -216,6 +218,8 @@ public enum WellKnownType "Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute", "Microsoft.AspNetCore.Mvc.ModelBinding.EmptyBodyBehavior", "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute", - "Microsoft.AspNetCore.Authorization.AuthorizeAttribute" + "Microsoft.AspNetCore.Authorization.AuthorizeAttribute", + "Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions", + "Microsoft.AspNetCore.Authorization.AuthorizationOptions" }; }