using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Nuuru.Tools.AuthFlowChart.Models;
namespace Nuuru.Tools.AuthFlowChart.Analysis;
///
/// Extracts authorization-related attributes from classes and methods
///
public class AttributeVisitor : CSharpSyntaxWalker
{
public string? RoutePrefix { get; private set; }
public AuthLevel AuthLevel { get; private set; } = AuthLevel.Anonymous;
public string? PolicyPermission { get; private set; }
public bool HasAuthorize { get; private set; }
public bool HasAllowAnonymous { get; private set; }
public static (AuthLevel level, string? permission, string? route) ExtractFromClass(ClassDeclarationSyntax classDecl)
{
var visitor = new AttributeVisitor();
visitor.VisitClassAttributes(classDecl);
return (visitor.AuthLevel, visitor.PolicyPermission, visitor.RoutePrefix);
}
public static (AuthLevel level, string? permission, string? httpMethod, string? route) ExtractFromMethod(MethodDeclarationSyntax method)
{
var visitor = new AttributeVisitor();
visitor.VisitMethodAttributes(method);
return (visitor.AuthLevel, visitor.PolicyPermission, visitor.GetHttpMethod(method), visitor.RoutePrefix);
}
private void VisitClassAttributes(ClassDeclarationSyntax classDecl)
{
foreach (var attrList in classDecl.AttributeLists)
{
foreach (var attr in attrList.Attributes)
{
ProcessAttribute(attr);
}
}
}
private void VisitMethodAttributes(MethodDeclarationSyntax method)
{
foreach (var attrList in method.AttributeLists)
{
foreach (var attr in attrList.Attributes)
{
ProcessAttribute(attr);
}
}
}
private void ProcessAttribute(AttributeSyntax attr)
{
var name = GetAttributeName(attr);
switch (name)
{
case "Route":
case "RouteAttribute":
RoutePrefix = ExtractFirstStringArgument(attr);
break;
case "AllowAnonymous":
case "AllowAnonymousAttribute":
HasAllowAnonymous = true;
AuthLevel = AuthLevel.Anonymous;
PolicyPermission = null;
break;
case "Authorize":
case "AuthorizeAttribute":
if (!HasAllowAnonymous) // AllowAnonymous overrides Authorize
{
HasAuthorize = true;
var policy = ExtractPolicyArgument(attr);
if (!string.IsNullOrEmpty(policy))
{
AuthLevel = AuthLevel.Permission;
PolicyPermission = ResolvePermissionConstant(policy);
}
else
{
AuthLevel = AuthLevel.Authenticated;
}
}
break;
case "HttpGet":
case "HttpGetAttribute":
case "HttpPost":
case "HttpPostAttribute":
case "HttpPut":
case "HttpPutAttribute":
case "HttpDelete":
case "HttpDeleteAttribute":
case "HttpPatch":
case "HttpPatchAttribute":
// Extract route template if present
var routeArg = ExtractFirstStringArgument(attr);
if (!string.IsNullOrEmpty(routeArg))
{
RoutePrefix = routeArg;
}
break;
}
}
private string GetHttpMethod(MethodDeclarationSyntax method)
{
foreach (var attrList in method.AttributeLists)
{
foreach (var attr in attrList.Attributes)
{
var name = GetAttributeName(attr);
if (name.StartsWith("Http"))
{
return name.Replace("Attribute", "").Replace("Http", "").ToUpperInvariant();
}
}
}
return "GET"; // Default
}
private static string GetAttributeName(AttributeSyntax attr)
{
var name = attr.Name.ToString();
// Handle qualified names like Microsoft.AspNetCore.Authorization.Authorize
var lastDot = name.LastIndexOf('.');
if (lastDot >= 0)
{
name = name[(lastDot + 1)..];
}
return name;
}
private static string? ExtractFirstStringArgument(AttributeSyntax attr)
{
if (attr.ArgumentList == null || attr.ArgumentList.Arguments.Count == 0)
return null;
var firstArg = attr.ArgumentList.Arguments[0];
if (firstArg.Expression is LiteralExpressionSyntax literal &&
literal.IsKind(SyntaxKind.StringLiteralExpression))
{
return literal.Token.ValueText;
}
return null;
}
private static string? ExtractPolicyArgument(AttributeSyntax attr)
{
if (attr.ArgumentList == null)
return null;
foreach (var arg in attr.ArgumentList.Arguments)
{
// Look for Policy = "..."
if (arg.NameEquals?.Name.Identifier.Text == "Policy")
{
if (arg.Expression is LiteralExpressionSyntax literal)
{
return literal.Token.ValueText;
}
// Handle Permissions.User.UploadPost reference
if (arg.Expression is MemberAccessExpressionSyntax memberAccess)
{
return memberAccess.ToString();
}
}
// Also handle positional argument if it's the policy
if (arg.NameEquals == null && arg.NameColon == null)
{
if (arg.Expression is MemberAccessExpressionSyntax memberAccess)
{
return memberAccess.ToString();
}
if (arg.Expression is LiteralExpressionSyntax literal)
{
return literal.Token.ValueText;
}
}
}
return null;
}
///
/// Resolve permission constants like "Permissions.User.UploadPost" to their string values
///
private static string ResolvePermissionConstant(string value)
{
// If it's already a string value (e.g., "user.upload_post"), return it
if (!value.StartsWith("Permissions."))
return value;
// Map known permission constants to their values
// This is populated from the Permissions.cs file
return value switch
{
"Permissions.User.UploadPost" => "user.upload_post",
"Permissions.User.Comment" => "user.comment",
"Permissions.User.EditOwnContent" => "user.edit_own_content",
"Permissions.User.DeleteOwnContent" => "user.delete_own_content",
"Permissions.User.EditTags" => "user.edit_tags",
"Permissions.Moderation.TrashPost" => "moderation.trash_post",
"Permissions.Moderation.DeleteComment" => "moderation.delete_comment",
"Permissions.Moderation.EditTags" => "moderation.edit_tags",
"Permissions.Moderation.BanUser" => "moderation.ban_user",
"Permissions.Moderation.ViewReports" => "moderation.view_reports",
"Permissions.Moderation.ViewAuditLog" => "moderation.view_audit_log",
"Permissions.Admin.ManageUsers" => "admin.manage_users",
"Permissions.Admin.ManagePermissions" => "admin.manage_permissions",
"Permissions.Admin.SystemSettings" => "admin.system_settings",
_ => value // Return as-is if not found
};
}
}