mo-esmp / DynamicRoleBasedAuthorizationNETCore

Dynamic Role-Based Access Control for ASP.NET Core MVC and Web API
GNU General Public License v3.0
451 stars 94 forks source link

Get List<Menu> based on user role or claim #30

Open fasteddys opened 2 years ago

fasteddys commented 2 years ago

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.

mo-esmp commented 2 years ago

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

fasteddys commented 2 years ago

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 });
mo-esmp commented 2 years ago

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.

fasteddys commented 2 years ago

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

Adding secure-content I think your approach is staticwhere 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>
mo-esmp commented 2 years ago

@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.

fasteddys commented 2 years ago

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 commented 2 years ago

Hello @mo-esmp bump +1 😄

mo-esmp commented 2 years ago

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?

fasteddys commented 2 years ago

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");
    }
}
mo-esmp commented 2 years ago

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

fasteddys commented 1 year ago

@mo-esmp not the expert here, but lets just start with something straight forward _PartialMenuor 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 runtimeby 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/

mo-esmp commented 1 year ago

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

fasteddys commented 4 months ago

hi bump