Search code examples
entity-framework-coreasp.net-core-identity

Creating dynamic role-based authorization


I have implemented Identity Role-based authorization but having to manually go to each controller/action and specify individually [Authorize(Roles = "")] very poor extensibility.

How would I be able to create a UI screen, with dynamic role-based authorization, where the "super admin" can configure which role has access to a controller/action?

Something like this:

Portal: Role function


Solution

  • After a lot of trial and error and a lot of research, I found an adequate answer:(big thanks to Mohen 'mo-esmp' Esmailpour)

    Create 2 class:

    public class MvcControllerInfo
    {
        public string Id => $"{AreaName}:{Name}";
    
        public string Name { get; set; }
        public string DisplayName { get; set; }
        public string AreaName { get; set; }
    
        public IEnumerable<MvcActionInfo> Actions { get; set; }
    }
    
    public class MvcActionInfo
    {
        public string Id => $"{ControllerId}:{Name}";
    
        public string Name { get; set; }
        public string DisplayName { get; set; }
        public string ControllerId { get; set; }
    }
    

    Add another class MvcControllerDiscovery to Services folder to discover all controllers and actions:

    public class MvcControllerDiscovery : IMvcControllerDiscovery
    {
        private List<MvcControllerInfo> _mvcControllers;
        private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
    
        public MvcControllerDiscovery(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
        {
            _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
        }
    
        public IEnumerable<MvcControllerInfo> GetControllers()
        {
            if (_mvcControllers != null)
                return _mvcControllers;
    
            _mvcControllers = new List<MvcControllerInfo>();
    
            var items = _actionDescriptorCollectionProvider
                .ActionDescriptors.Items
                .Where(descriptor => descriptor.GetType() == typeof(ControllerActionDescriptor))
                .Select(descriptor => (ControllerActionDescriptor)descriptor)
                .GroupBy(descriptor => descriptor.ControllerTypeInfo.FullName)
                .ToList();
    
            foreach (var actionDescriptors in items)
            {
                if (!actionDescriptors.Any())
                    continue;
    
                var actionDescriptor = actionDescriptors.First();
                var controllerTypeInfo = actionDescriptor.ControllerTypeInfo;
                var currentController = new MvcControllerInfo
                {
                    AreaName = controllerTypeInfo.GetCustomAttribute<AreaAttribute>()?.RouteValue,
                    DisplayName = controllerTypeInfo.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName,
                    Name = actionDescriptor.ControllerName,
                };
    
                var actions = new List<MvcActionInfo>();
    
                foreach (var descriptor in actionDescriptors.GroupBy(a => a.ActionName).Select(g => g.First()))
                {
                    var methodInfo = descriptor.MethodInfo;
                    actions.Add(new MvcActionInfo
                    {
                        ControllerId = currentController.Id,
                        Name = descriptor.ActionName,
                        DisplayName = methodInfo.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName,
                    });
                }
    
                currentController.Actions = actions;
                _mvcControllers.Add(currentController);
            }
    
            return _mvcControllers;
        }
    }
    

    IActionDescriptorCollectionProvider provides the cached collection of ActionDescriptor which each descriptor represents an action. Open Startup class and inside Configure method and register MvcControllerDiscovery dependency.

    services.AddSingleton<IMvcControllerDiscovery, MvcControllerDiscovery>();
    

    add role controller to manage roles. In Controller folder create RoleController then add Create action:

    public class RoleController : Controller
    {
        private readonly IMvcControllerDiscovery _mvcControllerDiscovery;
    
        public RoleController(IMvcControllerDiscovery mvcControllerDiscovery)
        {
            _mvcControllerDiscovery = mvcControllerDiscovery;
        }
    
        // GET: Role/Create
        public ActionResult Create()
        {
            ViewData["Controllers"] = _mvcControllerDiscovery.GetControllers();
    
            return View();
        }
    }
    

    Create class RoleViewModel in the Models directory:

    public class RoleViewModel
    {
        [Required]
        [StringLength(256, ErrorMessage = "The {0} must be at least {2} characters long.")]
        public string Name { get; set; }
    
        public IEnumerable<MvcControllerInfo> SelectedControllers { get; set; }
    }
    

    And in View folder add another folder and name it Role then add Create.cshtml view. I used jQuery.bonsai for showing controller and action hierarchy.

    @model RoleViewModel
    
    @{
        ViewData["Title"] = "Create Role";
        var controllers = (IEnumerable<MvcControllerInfo>)ViewData["Controllers"];
    }
    
    @section Header {
        <link href="~/lib/jquery-bonsai/jquery.bonsai.css" rel="stylesheet" />
    }
    
    <h2>Create Role</h2>
    
    <hr />
    <div class="row">
        <div class="col-md-6">
            <form asp-action="Create" class="form-horizontal">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Name" class="control-label col-md-2"></label>
                    <div class="col-md-10">
                        <input asp-for="Name" class="form-control" />
                        <span asp-validation-for="Name" class="text-danger"></span>
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-md-3 control-label">Access List</label>
                    <div class="col-md-9">
                        <ol id="tree">
                            @foreach (var controller in controllers)
                            {
                                string name;
                                {
                                    name = controller.DisplayName ?? controller.Name;
                                }
                                <li class="controller" data-value="@controller.Name">
                                    <input type="hidden" class="area" value="@controller.AreaName" />
                                    @name
                                    @if (controller.Actions.Any())
                                    {
                                        <ul>
                                            @foreach (var action in controller.Actions)
                                            {
                                                {
                                                    name = action.DisplayName ?? action.Name;
                                                }
                                                <li data-value="@action.Name">@name</li>
                                            }
                                        </ul>
                                    }
                                </li>
                            }
                        </ol>
                    </div>
                </div>
                <div class="form-group">
                    <input type="submit" value="Create" class="btn btn-default" />
                </div>
            </form>
        </div>
    </div>
    
    <div>
        <a asp-action="Index">Back to List</a>
    </div>
    
    @section Scripts {
        @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
        <script src="~/lib/jquery-qubit/jquery.qubit.js"></script>
        <script src="~/lib/jquery-bonsai/jquery.bonsai.js"></script>
        <script>
            $(function () {
                $('#tree').bonsai({
                    expandAll: false,
                    checkboxes: true,
                    createInputs: 'checkbox'
                });
    
                $('form').submit(function () {
                    var i = 0, j = 0;
                    $('.controller > input[type="checkbox"]:checked, .controller > input[type="checkbox"]:indeterminate').each(function () {
                        var controller = $(this);
                        if ($(controller).prop('indeterminate')) {
                            $(controller).prop("checked", true);
                        }
                        var controllerName = 'SelectedControllers[' + i + ']';
                        $(controller).prop('name', controllerName + '.Name');
    
                        var area = $(controller).next().next();
                        $(area).prop('name', controllerName + '.AreaName');
    
                        $('ul > li > input[type="checkbox"]:checked', $(controller).parent()).each(function () {
                            var action = $(this);
                            var actionName = controllerName + '.Actions[' + j + '].Name';
                            $(action).prop('name', actionName);
                            j++;
                        });
                        j = 0;
                        i++;
                    });
    
                    return true;
                });
            });
        </script>
    }
    

    This should get you to show each action in all controllers in the front-end to customize permission access for whichever role.

    If you don't have a class inheriting from identity user you can follow the rest of the steps at the link below to show how to set a role to a specific user. Good luck!

    https://github.com/mo-esmp/DynamicRoleBasedAuthorizationNETCore/blob/master/README.md

    Hope this helps.