DynamicRoleBasedAuthorizationNETCore icon indicating copy to clipboard operation
DynamicRoleBasedAuthorizationNETCore copied to clipboard

Get List<Menu> based on user role or claim

Open fasteddys opened this issue 3 years ago • 13 comments

Hello, thanks for this, its very creative approach and unique, is it possible to build a menu hierarchy based on the users roles/claims at startup/cached, could you please show us so I can tie the Mo-esmpMenuHelper into a _SideBarLayout

I can help with adding the BS 5 styles with a PR if you want.

fasteddys avatar Jun 11 '22 20:06 fasteddys

Hey @fasteddys , Please take a look at this issue and if you had further questions, please let me know.

mo-esmp avatar Jun 12 '22 19:06 mo-esmp

Thanks for that @mo-esmp this is a great lib, very smart and intelligent.

Since a user can have more than one roles, I was hoping for something just a little bit different, in the previous approach the entire menu is exposed even to unauthorized users, I understand they will be blocked when they access.

GetControllersActionsByRole()
GetControllersActionsByRoleFromCache()

Is there a way to get just the list/map collection of allowed by that role

Previously I used to do this... which is not really helpful, because I need get all users roles and then find the controller/actions.

var users  = UserManager.Users.Where(x=>x.Roles.Any(y=>y.RoleId==role.Id))
                    .Select(x => new {UserId = x.Id,FullName = x.FullName })
                    // how to get the controllers
                    .Select(z => new {controllerId= x.Id });

fasteddys avatar Jun 15 '22 17:06 fasteddys

You mean with secure-content tag helper still menu will be exposed to all users even unauthorized ones? I didn't get what you mean exactly, you're mentioning this library or your implementation. This library saves the controllers and actions for each role in RoleAccess table and then checks the requested controller/action with RoleAccess tables.

mo-esmp avatar Jun 15 '22 21:06 mo-esmp

Hello @mo-esmp, please correct me if I am wrong.

Adding secure-content I think your approach is static where I have to create HTML first. I wanted to generate the menus dynamically, since they could be role based etc. I was thinking by adding this GetControllersActionsByRole() , GetControllersActionsByRoleFromCache() and maybe breadcrumbs from below lib would be nice too.

Feel free to checkout Breadcrumbs https://github.com/zHaytam/SmartBreadcrumbs


Here is a some sample stuff I was trying to use, but the ASP Core MVC views does not have any good options.

Let me also share with you that your lib. is so flexible, I can manage parts of view, for e.g. inside a dashboard I can manage widget access as well!! 👍 very cool

[Generator]
public class MenuPagesGenerator : ISourceGenerator
{
    private const string RouteAttributeName = "Microsoft.AspNetCore.Components.RouteAttribute";
    private const string DescriptionAttributeName = "System.ComponentModel.Description";
    
    public void Initialize(GeneratorInitializationContext context) { }

    public void Execute(GeneratorExecutionContext context)
    {
        try
        {
            var menuComponents = GetMenuComponents(context.Compilation);

            var pageDetailsSource = SourceText.From(Templates.MenuPages(menuComponents), Encoding.UTF8);
            context.AddSource("PageDetails", pageDetailsSource);
            context.AddSource("PageDetail", SourceText.From(Templates.PageDetail(), Encoding.UTF8));
        }
        catch (Exception)
        {
            Debugger.Launch();
        }
    }

    private static ImmutableArray<RouteableComponent> GetMenuComponents(Compilation compilation)
    {
        // Get all classes
        IEnumerable<SyntaxNode> allNodes = compilation.SyntaxTrees.SelectMany(s => s.GetRoot().DescendantNodes());
        IEnumerable<ClassDeclarationSyntax> allClasses = allNodes
            .Where(d => d.IsKind(SyntaxKind.ClassDeclaration))
            .OfType<ClassDeclarationSyntax>();
        
        return allClasses
            .Select(component => TryGetMenuComponent(compilation, component))
            .Where(page => page is not null)
            .Cast<RouteableComponent>()// stops the nullable lies
            .ToImmutableArray();
    }
    
    private static RouteableComponent? TryGetMenuComponent(Compilation compilation, ClassDeclarationSyntax component)
    {
        var attributes = component.AttributeLists
            .SelectMany(x => x.Attributes)
            .Where(attr => 
                attr.Name.ToString() == RouteAttributeName
                || attr.Name.ToString() == DescriptionAttributeName)
            .ToList();

        var routeAttribute = attributes.FirstOrDefault(attr => attr.Name.ToString() == RouteAttributeName);
        var descriptionAttribute = attributes.FirstOrDefault(attr => attr.Name.ToString() == DescriptionAttributeName);

        if (routeAttribute is null || descriptionAttribute is null)
        {
            return null;
        }
            
        if (
            routeAttribute.ArgumentList?.Arguments.Count != 1 ||
            descriptionAttribute.ArgumentList?.Arguments.Count != 1)
        {
            // no route path or description value
            return null;
        }
            
        var semanticModel = compilation.GetSemanticModel(component.SyntaxTree);

        var routeArg = routeAttribute.ArgumentList.Arguments[0];
        var routeExpr = routeArg.Expression;
        var routeTemplate = semanticModel.GetConstantValue(routeExpr).ToString();
        
        var descriptionArg = descriptionAttribute.ArgumentList.Arguments[0];
        var descriptionExpr = descriptionArg.Expression;
        var title = semanticModel.GetConstantValue(descriptionExpr).ToString();

        return new RouteableComponent(routeTemplate, title);
    }
}

https://andrewlock.net/using-source-generators-to-generate-a-nav-component-in-a-blazor-app/


Currently.. similarl concept.. but I want to make this fully dynami, resuable so others can also use in the lib

<li class="dropdown">
    <a asp-controller="Drink" // get controller names
       asp-action="Index"
       class="dropdown-toggle" data-toggle="dropdown">Drinks<b class="caret"></b></a>
    <ul class="dropdown-menu">
        @foreach (var category in Model)
        {
            <li>
                <a asp-controller="Drink" asp-action="List"
                   asp-route-category="@category.CategoryName">@category.CategoryName</a>
            </li>
        }
        <li class="divider"></li>
        <li>
            <a asp-controller="Drink" asp-action="List" asp-route-category="">View all drinks</a>
        </li>
    </ul>
</li>

fasteddys avatar Jul 07 '22 13:07 fasteddys

@fasteddys Thanks for sharing the code and sorry for the late reply. I think we can divide this feature into 3 sections:

  1. A service to return a model that contains a list controller, action and name to generate links
  2. A tag helper or generator to render the menu (have no idea how the HTML part should look like? does it fit all?)
  3. Adding cash to improve performance

I can help with the implementation of 1 and 3 :) It would be great if create a sample project and assemble all codes in a project to see how they're working.

mo-esmp avatar Jul 09 '22 08:07 mo-esmp

thanks @mo-esmp sure, I can test it, happy to see you open to new ideas and pull our code snippets into one consolidate idea!! 🤗 it

fasteddys avatar Jul 15 '22 17:07 fasteddys

Hello @mo-esmp bump +1 😄

fasteddys avatar Aug 24 '22 00:08 fasteddys

Hey @fasteddys,

I guess I can start implementation this weekend but before that do think IMvcControllerDiscovery and MvcControllerInfo can help in case of having list of controllers and actions to generate links?

mo-esmp avatar Aug 24 '22 07:08 mo-esmp

Sure, I trust your design approach, looks interesting. For our users, lets do that & allow our developers who add our ms-es nuget package to easily add the menu with role/claims functionality

  1. Register in the program/configure section
  2. And generate a simple _menuPartial or _sideBar I saw this article...

public class HomeController : Controller
{
    [ChildActionOnly]
    public ActionResult RenderMenu()
    {
        return PartialView("_MenuBar");
    }
}

fasteddys avatar Aug 25 '22 00:08 fasteddys

@fasteddys I'm a bit confused. To create the menu in a dynamic way, what are the requirements?

  • For now, there is a method to get the list of controllers and actions for generating the menu, what else is required except caching?
  • The main question is how will the user change or inject HTML for creating the menu?

mo-esmp avatar Aug 29 '22 12:08 mo-esmp

@mo-esmp not the expert here, but lets just start with something straight forward _PartialMenu or menu ViewComponent that's perhaps a MenuRolesHashMap. Let others chime in with their ideas as it gets some use.

  1. Either a Menu created at compile time from an app.config / xml file (from an exported list menu action + roles)
  2. or created using the Startup() at runtime by getting the list of menu (Controllers/Actions) + roles.

To weave HTML, here's a couple of options either the template engine or pull it via a dynamic component, = User defined HTML + MoRoleAllowedMenuModel & render this

  1. templating engines like https://github.com/sebastienros/fluid
  2. https://colorlib.com/wp/top-templating-engines-for-javascript/
  3. Or we could, let the user create a UserCustomHtmlListSection(and pass in your ** MoMenuModelForCUrrentRole), which we can pull as a Render in our _layout/Master
 var htmlWriter = new HtmlTextWriter(stringWriter);
        base.Render(htmlWriter);

Breadcrumbs Sample - https://github.com/zHaytam/SmartBreadcrumbs https://blog.zhaytam.com/2018/06/24/asp-net-core-using-smartbreadcrumbs/ https://dotnetthoughts.net/implementing-breadcrumbs-in-aspnetcore/

fasteddys avatar Sep 02 '22 03:09 fasteddys

Let's keep this open. It will take time but in the end, we will implement this feature.

mo-esmp avatar Sep 08 '22 21:09 mo-esmp