File: Mvc\DetectOverriddenAuthorizeAttributeTest.cs
Web Access
Project: src\src\Framework\AspNetCoreAnalyzers\test\Microsoft.AspNetCore.App.Analyzers.Test.csproj (Microsoft.AspNetCore.App.Analyzers.Test)
// 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.CSharpAnalyzerVerifier<Microsoft.AspNetCore.Analyzers.Mvc.MvcAnalyzer>;
 
namespace Microsoft.AspNetCore.Analyzers.Mvc;
 
public partial class DetectOverriddenAuthorizeAttributeTest
{
    private const string CommonPrefix = """
using System;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
 
WebApplication.Create().Run();
 
""";
 
    [Fact]
    public async Task AuthorizeOnAction_AllowAnonymousOnController_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyController
{
    [{|#0:Authorize|}]
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyController").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AllowAnonymousOnAction_AuthorizeOnController_NoDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[Authorize]
public class MyController
{
    [AllowAnonymous]
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source);
    }
 
    [Fact]
    public async Task AuthorizeOnAction_AllowAnonymousOnControllerBase_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyControllerBase
{
}
 
public class MyController : MyControllerBase
{
    [{|#0:Authorize|}]
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AuthorizeOnActionControllerAndAction_AllowAnonymousOnControllerBase_HasMultipleDiagnostics()
    {
        // The closest Authorize attribute to the action reported if multiple could be considered overridden.
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
[{|#0:Authorize|}]
public class MyControllerBase
{
}
 
[{|#1:Authorize|}]
public class MyController : MyControllerBase
{
    [{|#2:Authorize|}]
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase").WithLocation(0),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase").WithLocation(1),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase").WithLocation(2)
        );
    }
 
    [Fact]
    public async Task AuthorizeOnController_AllowAnonymousOnControllerBase_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyControllerBase
{
}
 
[{|#0:Authorize|}]
public class MyController : MyControllerBase
{
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AuthorizeOnControllerWithMultipleActions_AllowAnonymousOnControllerBase_HasSingleDiagnostic()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyControllerBase
{
}
 
[{|#0:Authorize|}]
public class MyController : MyControllerBase
{
    public object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AuthorizeOnControllerBaseWithMultipleChildren_AllowAnonymousOnControllerBaseBaseType_HasMultipleDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyControllerBaseBase
{
}
 
[{|#0:Authorize|}]
public class MyControllerBase : MyControllerBaseBase
{
}
 
public class MyController : MyControllerBase
{
    public object Get() => new();
    public object AnotherGet() => new();
}
 
public class MyOtherController : MyControllerBase
{
    public object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        // This is not ideal. If someone comes along and fixes it to be a single diagnostic, feel free to update this test to end with
        // "HasSingleDiagnostic" instead of "HasMultipleDiagnostics". I think fixing it will require disabling parallelization of the
        // entire MvcAnalyzer which I don't think is a good tradeoff. I assume that this scenario is rare enough and that overreporting
        // is benign enough to not warrant the performance hit.
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase").WithLocation(0),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task CustomAuthorizeOnAction_CustomAllowAnonymousOnController_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[MyAllowAnonymous]
public class MyController
{
    [{|#0:MyAuthorize|}]
    public object Get() => new();
}
 
public class MyAuthorizeAttribute : Attribute, IAuthorizeData
{
    public string? Policy { get; set; }
    public string? Roles { get; set; }
    public string? AuthenticationSchemes { get; set; }
}
 
public class MyAllowAnonymousAttribute : Attribute, IAllowAnonymous
{
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyController").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AuthorizeOnAction_NonInheritableAllowAnonymousOnController_NoDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[MyAllowAnonymous]
public class MyControllerBase
{
}
 
public class MyController : MyControllerBase
{
    [Authorize]
    public object Get() => new();
}
 
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class MyAllowAnonymousAttribute : Attribute, IAllowAnonymous
{
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source);
    }
 
    [Fact]
    public async Task CustomAuthorizeCombinedWithAllowAnonymousOnAction_AllowAnonymousOnController_NoDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyController
{
    [MyAuthorize]
    public object Get() => new();
}
 
public class MyAuthorizeAttribute : Attribute, IAuthorizeData, IAllowAnonymous
{
    public string? Policy { get; set; }
    public string? Roles { get; set; }
    public string? AuthenticationSchemes { get; set; }
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source);
    }
 
    [Fact]
    public async Task AuthorizeBeforeAllowAnonymousOnAction_AllowAnonymousOnController_NoDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyController
{
    [Authorize(AuthenticationSchemes = "foo")]
    [AllowAnonymous]
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source);
    }
 
    [Fact]
    public async Task AuthorizeAfterAllowAnonymousOnAction_NoAttributeOnController_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
public class MyController
{
    [AllowAnonymous]
    [{|#0:Authorize|}]
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyController.Get").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task NoAttributeOnAction_AuthorizeBeforeAllowAnonymousOnController_NoDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[Authorize(AuthenticationSchemes = "foo")]
[AllowAnonymous]
public class MyController
{
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source);
    }
 
    [Fact]
    public async Task NoAttributeOnAction_AuthorizeAfterAllowAnonymousOnController_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
[{|#0:Authorize|}]
public class MyController
{
    public object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyController").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AuthorizeOnAction_AllowAnonymousOnBaseMethod_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
public class MyControllerBase
{
    [AllowAnonymous]
    public virtual object Get() => new();
}
 
public class MyController : MyControllerBase
{
    [{|#0:Authorize|}]
    public override object Get() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase.Get").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AllowAnonymousOnVirtualBaseActionWithNoOverride_AuthorizeOnController_NoDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
public class MyControllerBase
{
    [AllowAnonymous]
    public virtual object Get() => new();
}
 
[Authorize]
public class MyController : MyControllerBase
{
}
""";
 
        // I suspect [AllowAnonymous] on a specific action that never needs auth despite the child controller
        // configuring auth is a fairly common pattern. However, if the action is explicitly overridden,
        // you need to reapply [AllowAnonymous] on the override to silence the warning and clarify your intent.
        // Making people do this when there isn't even an override is a bit onerous.
        await VerifyCS.VerifyAnalyzerAsync(source);
    }
 
    [Fact]
    public async Task AllowAnonymousOnVirtualBaseActionAndOverride_AuthorizeOnController_NoDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
public class MyControllerBase
{
    [AllowAnonymous]
    public virtual object Get() => new();
}
 
[Authorize]
public class MyController : MyControllerBase
{
    [AllowAnonymous]
    public override object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source);
    }
 
    [Fact]
    public async Task AllowAnonymousOnVirtualBaseBaseActionButNotOverride_AuthorizeOnControllerBase_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
public class MyControllerBaseBase
{
    [AllowAnonymous]
    public virtual object Get() => new();
}
 
[{|#0:Authorize|}]
public class MyControllerBase : MyControllerBaseBase
{
}
 
public class MyController : MyControllerBase
{
    public override object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase.Get").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AllowAnonymousOnVirtualBaseActionButNotOverride_AuthorizeOnController_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
public class MyControllerBase
{
    [AllowAnonymous]
    public virtual object Get() => new();
}
 
[{|#0:Authorize|}]
public class MyController : MyControllerBase
{
    public override object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBase.Get").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AllowAnonymousOnBaseBaseController_AuthorizeOnControllerBase_HasSingleDiagnostic()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyControllerBaseBase
{
    public virtual object Get() => new();
}
 
[{|#0:Authorize|}]
public class MyControllerBase : MyControllerBaseBase
{
}
 
public class MyController : MyControllerBase
{
    public override object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase").WithLocation(0)
        );
    }
 
    [Fact]
    public async Task AllowAnonymousOnControllerBaseBaseType_AuthorizeOnControllerBaseAndAction_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class MyControllerBaseBase
{
    [{|#2:Authorize|}]
    public virtual object Get() => new();
}
 
[{|#0:Authorize|}]
public class MyControllerBase : MyControllerBaseBase
{
    [{|#1:Authorize|}]
    public override object Get() => new();
}
 
public class MyController : MyControllerBase
{
    public override object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase").WithLocation(0),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase").WithLocation(1),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase").WithLocation(2)
        );
    }
 
    [Fact]
    public async Task AllowAnonymousOnBaseBaseType_AuthorizeOnControllerBaseAndAction_HasDiagnostics()
    {
        var source = $$"""
{{CommonPrefix}}
[Authorize]
public class MyControllerBaseBase
{
    [AllowAnonymous]
    public virtual object Get() => new();
}
 
[{|#0:Authorize|}]
public class MyControllerBase : MyControllerBaseBase
{
    [{|#1:Authorize|}]
    public override object Get() => new();
}
 
public class MyController : MyControllerBase
{
    public override object Get() => new();
    public object AnotherGet() => new();
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase.Get").WithLocation(0),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("MyControllerBaseBase.Get").WithLocation(1)
        );
    }
 
    [Fact]
    public async Task AttributesOnMultipleActions_AttributesOnMultipleControllers_HasDiagnostics()
    {
        // Copied from https://github.com/dotnet/aspnetcore/issues/43550#issuecomment-1940287150
        var source = $$"""
{{CommonPrefix}}
[AllowAnonymous]
public class OAuthControllerAnon : ControllerBase
{
}
 
[Authorize]
public class OAuthControllerAuthz : ControllerBase
{
}
 
[{|#0:Authorize|}] // BUG
public class OAuthControllerInherited : OAuthControllerAnon
{
}
 
public class OAuthControllerInherited2 : OAuthControllerAnon
{
    [{|#1:Authorize|}] // BUG
    public IActionResult Privacy()
    {
        return null;
    }
}
 
[AllowAnonymous]
[{|#2:Authorize|}] // BUG
public class OAuthControllerMultiple : ControllerBase
{
}
 
[AllowAnonymous]
public class OAuthControllerInherited3 : ControllerBase
{
    [{|#3:Authorize|}] // BUG
    public IActionResult Privacy()
    {
        return null;
    }
}
""";
 
        await VerifyCS.VerifyAnalyzerAsync(source,
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("OAuthControllerAnon").WithLocation(0),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("OAuthControllerAnon").WithLocation(1),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("OAuthControllerMultiple").WithLocation(2),
            new DiagnosticResult(DiagnosticDescriptors.OverriddenAuthorizeAttribute).WithArguments("OAuthControllerInherited3").WithLocation(3)
        );
    }
}