Skip to content

Commit

Permalink
Add analyzer which check how ObjectAuthorizationRules applied (#4051)
Browse files Browse the repository at this point in the history
* Add analyzer which check how ObjectAuthorizationRules applied and make sure that methods is public and static.

Fixes #1561
Fixes #1560

* Address PR feedback

---------

Co-authored-by: Rockford Lhotka <rocky@lhotka.net>
  • Loading branch information
kant2002 and rockfordlhotka authored Jun 20, 2024
1 parent 3f19935 commit ea08ad3
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 1 deletion.
21 changes: 21 additions & 0 deletions Samples/BlazorExample/nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="clsa" value="C:\d\github\csla\NuGet\Packages" />
</packageSources>
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<!--<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="clsa">
<package pattern="Csla" />
<package pattern="Csla.*" />
</packageSource>-->
<clear />
</packageSourceMapping>

</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Csla.Analyzers.Tests
{
[TestClass]
public sealed class ObjectAuthorizationRulesAttributeAnalyzerTests
{
[TestMethod]
public void VerifySupportedDiagnostics()
{
var analyzer = new ObjectAuthorizationRulesAttributeAnalyzer();
var diagnostics = analyzer.SupportedDiagnostics;
Assert.AreEqual(3, diagnostics.Length);

var attributeMissingDiagnostic = diagnostics[0];
Assert.AreEqual(Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesAttributeMissing, attributeMissingDiagnostic.Id,
nameof(DiagnosticDescriptor.Id));
Assert.AreEqual(ObjectAuthorizationRulesAttributeAnalyzerConstants.AttributeMissingTitle, attributeMissingDiagnostic.Title.ToString(),
nameof(DiagnosticDescriptor.Title));
Assert.AreEqual(ObjectAuthorizationRulesAttributeAnalyzerConstants.AttributeMissingMessage, attributeMissingDiagnostic.MessageFormat.ToString(),
nameof(DiagnosticDescriptor.MessageFormat));
Assert.AreEqual(Constants.Categories.Usage, attributeMissingDiagnostic.Category,
nameof(DiagnosticDescriptor.Category));
Assert.AreEqual(DiagnosticSeverity.Warning, attributeMissingDiagnostic.DefaultSeverity,
nameof(DiagnosticDescriptor.DefaultSeverity));
Assert.AreEqual(HelpUrlBuilder.Build(Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesAttributeMissing, nameof(ObjectAuthorizationRulesAttributeAnalyzer)),
attributeMissingDiagnostic.HelpLinkUri,
nameof(DiagnosticDescriptor.HelpLinkUri));

var rulesConfigurationPublicDiagnostic = diagnostics[1];
Assert.AreEqual(Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesPublic, rulesConfigurationPublicDiagnostic.Id,
nameof(DiagnosticDescriptor.Id));
Assert.AreEqual(ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesPublicTitle, rulesConfigurationPublicDiagnostic.Title.ToString(),
nameof(DiagnosticDescriptor.Title));
Assert.AreEqual(ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesPublicMessage, rulesConfigurationPublicDiagnostic.MessageFormat.ToString(),
nameof(DiagnosticDescriptor.MessageFormat));
Assert.AreEqual(Constants.Categories.Usage, rulesConfigurationPublicDiagnostic.Category,
nameof(DiagnosticDescriptor.Category));
Assert.AreEqual(DiagnosticSeverity.Info, rulesConfigurationPublicDiagnostic.DefaultSeverity,
nameof(DiagnosticDescriptor.DefaultSeverity));
Assert.AreEqual(HelpUrlBuilder.Build(Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesPublic, nameof(ObjectAuthorizationRulesAttributeAnalyzer)),
rulesConfigurationPublicDiagnostic.HelpLinkUri,
nameof(DiagnosticDescriptor.HelpLinkUri));

var rulesConfigurationStaticDiagnostic = diagnostics[2];
Assert.AreEqual(Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesStatic, rulesConfigurationStaticDiagnostic.Id,
nameof(DiagnosticDescriptor.Id));
Assert.AreEqual(ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesStaticTitle, rulesConfigurationStaticDiagnostic.Title.ToString(),
nameof(DiagnosticDescriptor.Title));
Assert.AreEqual(ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesStaticMessage, rulesConfigurationStaticDiagnostic.MessageFormat.ToString(),
nameof(DiagnosticDescriptor.MessageFormat));
Assert.AreEqual(Constants.Categories.Usage, rulesConfigurationStaticDiagnostic.Category,
nameof(DiagnosticDescriptor.Category));
Assert.AreEqual(DiagnosticSeverity.Warning, rulesConfigurationStaticDiagnostic.DefaultSeverity,
nameof(DiagnosticDescriptor.DefaultSeverity));
Assert.AreEqual(HelpUrlBuilder.Build(Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesStatic, nameof(ObjectAuthorizationRulesAttributeAnalyzer)),
rulesConfigurationStaticDiagnostic.HelpLinkUri,
nameof(DiagnosticDescriptor.HelpLinkUri));
}

[TestMethod]
public async Task AnalyzeWhenClassIsNotMobileObject()
{
var code = "public class A { }";
await TestHelpers.RunAnalysisAsync<ObjectAuthorizationRulesAttributeAnalyzer>(code, []);
}

[TestMethod]
public async Task AnalyzeWhenClassIsMobileObjectAndObjectAuthorizationRulesHasAttribute()
{
var code =
"""
using Csla;

public class A : BusinessBase<A>
{
[ObjectAuthorizationRules]
public static void ConfigureObjectAuthorizationRules()
{
}
}
""";
await TestHelpers.RunAnalysisAsync<ObjectAuthorizationRulesAttributeAnalyzer>(code, []);
}

[TestMethod]
public async Task AnalyzeWhenClassIsMobileObjectAndDoesNotHaveObjectAuthorizationRules()
{
var code =
"""
using Csla;

public class A : BusinessBase<A>
{
[Fetch]
private void Fetch() { }
}
""";
await TestHelpers.RunAnalysisAsync<ObjectAuthorizationRulesAttributeAnalyzer>(code, []);
}

[TestMethod]
public async Task AnalyzeWhenClassIsMobileObjectAndOperationHasNamingConvention()
{
var code =
"""
using Csla;

public class A : BusinessBase<A>
{
public static void AddObjectAuthorizationRules()
{
}
}
""";
await TestHelpers.RunAnalysisAsync<ObjectAuthorizationRulesAttributeAnalyzer>(
code, [Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesAttributeMissing]);
}

[TestMethod]
public async Task ObjectAuthorizationRulesMethodShouldBePublic()
{
var code =
"""
using Csla;

public class A : BusinessBase<A>
{
[ObjectAuthorizationRules]
private static void AddObjectAuthorizationRules()
{
}
}
""";
await TestHelpers.RunAnalysisAsync<ObjectAuthorizationRulesAttributeAnalyzer>(
code, [Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesPublic]);
}

[TestMethod]
public async Task ObjectAuthorizationRulesMethodShouldBeStatic()
{
var code =
"""
using Csla;

public class A : BusinessBase<A>
{
[ObjectAuthorizationRules]
public void AddObjectAuthorizationRules()
{
}
}
""";
await TestHelpers.RunAnalysisAsync<ObjectAuthorizationRulesAttributeAnalyzer>(
code, [Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesStatic]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ CSLA0015 | Usage | Error | EvaluateOperationAttributeUsageAnalyzer
CSLA0016 | Usage | Error | AsynchronousBusinessRuleInheritingFromBusinessRuleAnalyzer
CSLA0017 | Usage | Warning | BusinessRuleDoesNotUseAddMethodsOnContextAnalyzer
CSLA0018 | Usage | Error | IsCompleteCalledInAsynchronousBusinessRuleAnalyzer
CSLA0019 | Usage | Error | FindRefAndOutParametersInOperationsAnalyzer
CSLA0019 | Usage | Error | FindRefAndOutParametersInOperationsAnalyzer
CSLA0020 | Usage | Warning | ObjectAuthorizationRulesAttributeMissing
CSLA0021 | Usage | Info | ObjectAuthorizationRulesPublic
CSLA0022 | Usage | Warning | ObjectAuthorizationRulesStatic
3 changes: 3 additions & 0 deletions Source/Csla.Analyzers/Csla.Analyzers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public static class AnalyzerIdentifiers
public const string BusinessRuleContextUsage = "CSLA0017";
public const string CompleteInExecuteAsync = "CSLA0018";
public const string RefOrOutParameterInOperation = "CSLA0019";
public const string ObjectAuthorizationRulesAttributeMissing = "CSLA0020";
public const string ObjectAuthorizationRulesPublic = "CSLA0021";
public const string ObjectAuthorizationRulesStatic = "CSLA0022";
}

public static class Categories
Expand Down
2 changes: 2 additions & 0 deletions Source/Csla.Analyzers/Csla.Analyzers/CslaMemberConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static class Operations
public const string ChildUpdate = "Child_Update";
public const string ChildDeleteSelf = "Child_DeleteSelf";
public const string ChildExecute = "Child_Execute";
public const string AddObjectAuthorizationRules = "AddObjectAuthorizationRules";
}

public static class OperationAttributes
Expand Down Expand Up @@ -64,6 +65,7 @@ public static class Types
public const string DataPortalChildOperationAttribute = nameof(DataPortalChildOperationAttribute);
public const string DataPortalOperationAttribute = nameof(DataPortalOperationAttribute);
public const string DataPortalRootOperationAttribute = nameof(DataPortalRootOperationAttribute);
public const string ObjectAuthorizationRulesAttribute = nameof(ObjectAuthorizationRulesAttribute);
public const string DynamicListBase = nameof(DynamicListBase);
public const string IBusinessObject = nameof(IBusinessObject);
public const string IMobileObject = nameof(IMobileObject);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Csla.Analyzers.Extensions
{
public readonly struct CslaOperationQualification
{
public CslaOperationQualification(bool byNamingConvention, bool byAttribute) =>
(ByNamingConvention, ByAttribute) = (byNamingConvention, byAttribute);

public static implicit operator bool(CslaOperationQualification qualification) =>
qualification.ByAttribute | qualification.ByNamingConvention;

public void Deconstruct(out bool byNamingConvention, out bool byAttribute) =>
(byNamingConvention, byAttribute) = (ByNamingConvention, ByAttribute);

public bool ByAttribute { get; }
public bool ByNamingConvention { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,20 @@ internal static DataPortalOperationQualification IsChildDataPortalOperation(this
return new DataPortalOperationQualification(byNamingConvention, byAttribute);
}
}

internal static CslaOperationQualification IsAddObjectAuthorizationRulesOperation(this IMethodSymbol @this)
{
if (@this is null)
{
return new CslaOperationQualification();
}
else
{
var byNamingConvention =
@this.Name == CslaMemberConstants.Operations.AddObjectAuthorizationRules;
var byAttribute = @this.GetAttributes().Any(_ => _.AttributeClass.IsObjectAuthorizationRulesAttribute());
return new CslaOperationQualification(byNamingConvention, byAttribute);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,13 @@ internal static bool IsMobileObject(this ITypeSymbol @this)
@this.ContainingAssembly.Name == CslaMemberConstants.AssemblyName) ||
(@this.BaseType.IsMobileObject() || @this.Interfaces.Any(_ => _.IsMobileObject())));
}

internal static bool IsObjectAuthorizationRulesAttribute(this ITypeSymbol @this)
{
return @this != null &&
((@this.Name == CslaMemberConstants.Types.ObjectAuthorizationRulesAttribute &&
@this.ContainingAssembly.Name == CslaMemberConstants.AssemblyName) ||
@this.BaseType.IsObjectAuthorizationRulesAttribute());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Csla.Analyzers.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

namespace Csla.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ObjectAuthorizationRulesAttributeAnalyzer
: DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor missingAttributeRule =
new DiagnosticDescriptor(
Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesAttributeMissing, ObjectAuthorizationRulesAttributeAnalyzerConstants.AttributeMissingTitle,
ObjectAuthorizationRulesAttributeAnalyzerConstants.AttributeMissingMessage, Constants.Categories.Usage,
DiagnosticSeverity.Warning, true,
helpLinkUri: HelpUrlBuilder.Build(
Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesAttributeMissing, nameof(ObjectAuthorizationRulesAttributeAnalyzer)));
private static readonly DiagnosticDescriptor shouldBePublicRule =
new DiagnosticDescriptor(
Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesPublic, ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesPublicTitle,
ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesPublicMessage, Constants.Categories.Usage,
DiagnosticSeverity.Info, true,
helpLinkUri: HelpUrlBuilder.Build(
Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesPublic, nameof(ObjectAuthorizationRulesAttributeAnalyzer)));
private static readonly DiagnosticDescriptor shouldBeStaticRule =
new DiagnosticDescriptor(
Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesStatic, ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesStaticTitle,
ObjectAuthorizationRulesAttributeAnalyzerConstants.RulesStaticMessage, Constants.Categories.Usage,
DiagnosticSeverity.Warning, true,
helpLinkUri: HelpUrlBuilder.Build(
Constants.AnalyzerIdentifiers.ObjectAuthorizationRulesStatic, nameof(ObjectAuthorizationRulesAttributeAnalyzer)));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(missingAttributeRule, shouldBePublicRule, shouldBeStaticRule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
}

private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
{
var methodNode = (MethodDeclarationSyntax)context.Node;

var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodNode);
var typeSymbol = methodSymbol.ContainingType;

if (typeSymbol.IsStereotype())
{
var qualification = methodSymbol.IsAddObjectAuthorizationRulesOperation();

if(qualification.ByNamingConvention && !qualification.ByAttribute)
{
context.ReportDiagnostic(Diagnostic.Create(
missingAttributeRule, methodSymbol.Locations[0]));
}
if (methodSymbol.DeclaredAccessibility != Accessibility.Public && (qualification.ByNamingConvention || qualification.ByAttribute))
{
context.ReportDiagnostic(Diagnostic.Create(
shouldBePublicRule, methodSymbol.Locations[0]));
}
if (!methodSymbol.IsStatic && (qualification.ByNamingConvention || qualification.ByAttribute))
{
context.ReportDiagnostic(Diagnostic.Create(
shouldBeStaticRule, methodSymbol.Locations[0]));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Csla.Analyzers
{
public static class ObjectAuthorizationRulesAttributeAnalyzerConstants
{
public const string AttributeMissingTitle = "Find Authorization Rules Configuration That Do Not Have an Operation Attribute";
public const string AttributeMissingMessage = "Authorization rules should use the appropriate operation attribute";
public const string RulesPublicTitle = "Find Authorization Rules Configuration That Is Not Public";
public const string RulesPublicMessage = "Authorization rules should be declared as public methods";
public const string RulesStaticTitle = "Find Authorization Rules Configuration That Is Not Static";
public const string RulesStaticMessage = "Authorization rules should be declared as static methods";
}

public static class ObjectAuthorizationRulesAttributeAnalyzerAddAttributeCodeFixConstants
{
public const string AddAttributeAndUsingDescription = "Add attribute and using statement";
public const string AddAttributeDescription = "Add attribute";
public const string CslaNamespace = "Csla";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Object authorization rules configuration should be marked with attribute.

## Issue
This analyzer is informing developer, that `AddObjectAuthorizationRules` method should be marked with `[ObjectAuthorizationRules]` attribute:

```
public class Customer
: BusinessBase<Customer>
{
public static void AddObjectAuthorizationRules()
{
}
}
```

Please add `[ObjectAuthorizationRules]` attribute to the method.

## Code Fix

No code fix exists for this analyzer.
Loading

0 comments on commit ea08ad3

Please sign in to comment.