Today we will learn Policy Based Authorization, PaginatedList, Managing user roles.
In this series, we learned till now
- Core configuration for entityframework and identity. (Part 01)
- Implementing register, login and logout functionalities. (Part 02)
- Seed default users, default roles in database and add hardcoded permissions to default super role. (Part 03)
The source code of previous parts and this part is available here.
Create a folder and name it Helpers. Under Helpers folder, create a file PaginatedList.cs and replace the content if there with the following.
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; namespace Identity.Helpers { public class PaginatedList<T> : List<T> { public int PageIndex { get; } public int TotalPages { get; } public PaginatedList(IEnumerable<T> items, int count, int pageIndex, int pageSize) : base(items) { PageIndex = pageIndex; TotalPages = (int)Math.Ceiling(count / (double)pageSize); } public bool HasPreviousPage => (PageIndex > 1); public bool HasNextPage => (PageIndex < TotalPages); public static PaginatedList<T> CreateFromLinqQueryable(IQueryable<T> source, int pageIndex, int pageSize) { var count = source.Count(); var items = source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); return new PaginatedList<T>(items, count, pageIndex, pageSize); } public static async Task<PaginatedList<T>> CreateFromEfQueryableAsync(IQueryable<T> source, int pageIndex, int pageSize) { var count = await source.CountAsync(); var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); return new PaginatedList<T>(items, count, pageIndex, pageSize); } } }
PaginatedList.cs is enough to create paginated list. We will display them accordingly. As we will move to permission based authorization, we need to implement policy based authorization alongside role based authorization. And both of them will work in our codebase according to their calling. I will try to write a post on how Policy Based Authorization works in future. The code is very intuitive, if you look closely you will understand most of it. If you are stuck somewhere, just comment and I will explain.
Under Identity project, create a folder & name it Authorization. Inside Authorization folder, create three file PermissionAuthorizationHandler.cs, PermissionPolicyProvider.cs, PermissionRequirement.cs . The content of these three files will be respectively.
PermissionAuthorizationHandler.cs
using Identity.Permissions; using Microsoft.AspNetCore.Authorization; using System.Linq; using System.Threading.Tasks; namespace Identity.Authorization { public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement> { protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { if ((context.User.Identity != null && !context.User.Identity.IsAuthenticated) || context.User.Identity == null) { context.Fail(); return; } var permissions = context.User.Claims.ToList(); if (permissions.Count == 0) { context.Fail(); return; } if (permissions.Any(x => x.Type == CustomClaimTypes.Permission && x.Value == requirement.Permission && x.Issuer == "LOCAL AUTHORITY")) { context.Succeed(requirement); return; } context.Fail(); } } }
PermissionPolicyProvider.cs
using Identity.Permissions; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; namespace Identity.Authorization { public class PermissionPolicyProvider : IAuthorizationPolicyProvider { public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } public PermissionPolicyProvider(IOptions<AuthorizationOptions> options) { FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); } public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { if (!policyName.StartsWith(CustomClaimTypes.Permission, StringComparison.OrdinalIgnoreCase)) return FallbackPolicyProvider.GetPolicyAsync(policyName); var policy = new AuthorizationPolicyBuilder(); policy.RequireAuthenticatedUser(); policy.AddRequirements(new PermissionRequirement(policyName)); return Task.FromResult(policy.Build()); } public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); } }
PermissionRequirement.cs
using Microsoft.AspNetCore.Authorization; namespace Identity.Authorization { public class PermissionRequirement : IAuthorizationRequirement { public string Permission { get; } public PermissionRequirement(string permission) { Permission = permission; } } }
Now to bring the effect of our custom policy based authorization, we need to register them in ConfigureServices Method in Startup class. Open Startup class and add following two lines in ConfigureServices Method.
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>(); services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
Now our policy based authorization is ready to go. Hey, I am going to disappoint you if you think we will complete the permission based authorization is this chapter. 😛 We are very closed to that. We will see that in next chapter.
Lets implement managing user roles functionality. Create two View Models inside ViewModels folder and name them ManageUserRolesViewModel.cs, UserViewModel.cs. Then contents of both files will be respectively.
ManageUserRolesViewModel.cs
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Identity.ViewModels { public class ManageUserRolesViewModel { [Required] public string UserId { get; set; } public string UserName { get; set; } public IList<ManageRoleViewModel> ManageRolesViewModel { get; set; } } public class ManageRoleViewModel { public string Name { get; set; } public string Description { get; set; } public bool Checked { get; set; } } }
UserViewModel.cs
namespace Identity.ViewModels { public class UserViewModel { public string Id { get; set; } public string UserName { get; set; } public string Email { get; set; } } }
Create a controller and name it UserController.cs .
In UserController class, there are four methods.
UserController(UserManager<AppUser> userManager, RoleManager<AppRole> roleManager)
This method is used to inject relevant dependencies.
Index(int? pageNumber, int? pageSize)
This is a GET method used to view all users. Look into this to learn how we are retrieving all users from database. This method has a respective razor view. We call that as a page when it is displayed in browser. From this page we will redirect to Manage Roles & Manage User Permissions. Manage User Permissions will be shown in next chapter.
ManageRoles(string userId)
This is a GET method responsible to retrieve user’s roles and all available roles in application. After retrieving them, it display them in way so that end user can assign/de-assign roles to user. This method has respective view.
ManageRoles(ManageUserRolesViewModel manageUserRolesViewModel)
After end user assign/de-assign roles and submit the form the request will be submitted to this method. Hence, it is a post method by same name as the previous method. It will receive an object of ManageUserRolesViewModel and will do relevant works to assign/de-assign roles.
To be noted, we will use permission based authorizations in this class. Observe closely the authorize attribute, and you will understand. The content of UserController.cs will be
using Identity.Helpers; using Identity.Models; using Identity.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Linq; using System.Threading.Tasks; namespace Identity.Controllers { public class UserController : Controller { private readonly UserManager<AppUser> _userManager; private readonly RoleManager<AppRole> _roleManager; public UserController(UserManager<AppUser> userManager, RoleManager<AppRole> roleManager) { _userManager = userManager; _roleManager = roleManager; } [HttpGet] [Authorize(Policy = Permissions.Permissions.Users.View)] public async Task<IActionResult> Index(int? pageNumber, int? pageSize) { var users = _userManager.Users.OrderBy(x => x.UserName); var rs = await PaginatedList<AppUser>.CreateFromEfQueryableAsync(users.AsNoTracking(), pageNumber ?? 1, pageSize ?? 12); var userViewModels = rs.Select(user => new UserViewModel {Id = user.Id, UserName = user.UserName, Email = user.Email}).ToList(); var response = new PaginatedList<UserViewModel>(userViewModels, rs.Count, pageNumber ?? 1, pageSize ?? 12); return View(response); } [HttpGet] [Authorize(Policy = Permissions.Permissions.Users.ManageRoles)] public async Task<IActionResult> ManageRoles(string userId) { var user = await _userManager.FindByIdAsync(userId); if (user == null) return View(); var userRoles = await _userManager.GetRolesAsync(user); var allRoles = await _roleManager.Roles.ToListAsync(); var allRolesViewModel = allRoles.Select(role => new ManageRoleViewModel {Name = role.Name, Description = role.Description}).ToList(); foreach (var roleViewModel in allRolesViewModel.Where(roleViewModel => userRoles.Contains(roleViewModel.Name))) { roleViewModel.Checked = true; } var manageUserRolesViewModel = new ManageUserRolesViewModel { UserId = userId, UserName = user.UserName, ManageRolesViewModel = allRolesViewModel }; return View(manageUserRolesViewModel); } [HttpPost] [Authorize(Policy = Permissions.Permissions.Users.ManageRoles)] public async Task<IActionResult> ManageRoles(ManageUserRolesViewModel manageUserRolesViewModel) { if (!ModelState.IsValid) return View(manageUserRolesViewModel); var user = await _userManager.FindByIdAsync(manageUserRolesViewModel.UserId); if (user == null) return View(manageUserRolesViewModel); var existingRoles = await _userManager.GetRolesAsync(user); foreach (var roleViewModel in manageUserRolesViewModel.ManageRolesViewModel) { var roleExists = existingRoles.FirstOrDefault(x => x == roleViewModel.Name); switch (roleViewModel.Checked) { case true when roleExists == null: await _userManager.AddToRoleAsync(user, roleViewModel.Name); break; case false when roleExists != null: await _userManager.RemoveFromRoleAsync(user, roleViewModel.Name); break; } } return RedirectToAction("Index", "User", new { id = manageUserRolesViewModel.UserId, succeeded = true, message = "Succeeded" }); } } }
In UserController.cs there are two GET methods that should have respective razor pages. Lets create them and write the codes. Under Views->User , Create two files Index.cshtml, ManageRoles.cshtml . The content of both files will be
Index.cshtml
@model Identity.Helpers.PaginatedList<Identity.ViewModels.UserViewModel> @{ ViewData["Title"] = "View Users"; Layout = "~/Views/Shared/_Layout.cshtml"; } <div class="card"> <div class="card-header"> <h3 class="card-title">Users</h3> </div> <div class="card-body"> <table class="table table-bordered table-hover"> <thead> <tr> <th> Id </th> <th> UserName </th> <th> Email </th> <th> Action </th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Id) </td> <td> @Html.DisplayFor(modelItem => item.UserName) </td> <td> @Html.DisplayFor(modelItem => item.Email) </td> <td> <a asp-action="ManageRoles" asp-route-userId="@item.Id">Manage Roles</a> | @*<a asp-action="ManagePermissions" asp-route-userId="@item.Id">Manage Permissions</a>*@ </td> </tr> } </tbody> </table> </div> <div class="card-footer clearfix"> @{ var prevDisabled = !Model.HasPreviousPage ? "disabled" : ""; var nextDisabled = !Model.HasNextPage ? "disabled" : ""; } <div class="row"> <div class="col"> <ul class="pagination float-right"> <li> <a asp-action="Index" asp-route-pageNumber="@(Model.PageIndex - 1)" class="btn btn-default @prevDisabled"> Previous </a> </li> @for (var i = 1; i <= Model.TotalPages; i++) { <li class="page-item @(i == Model.PageIndex ? "active" : "")" style="z-index: 0;"> <a asp-action="Index" asp-route-pageNumber="@i" class="page-link">@i</a> </li> } <li> <a asp-action="Index" asp-route-pageNumber="@(Model.PageIndex + 1)" class="btn btn-default @nextDisabled"> Next </a> </li> </ul> </div> </div> </div> </div>
ManageRoles.cshtml
@model Identity.ViewModels.ManageUserRolesViewModel @{ ViewData["Title"] = "Roles"; Layout = "~/Views/Shared/_Layout.cshtml"; } <div class="card"> <form method="post" asp-action="ManageRoles" class="d-inline"> <div class="card-header"> <h3 class="card-title">Manage Roles for User: <b>@Model.UserName</b></h3> <a asp-action="Index" class="float-right">See All Users</a> </div> <div class="card-body"> <input asp-for="@Model.UserId" type="hidden" /> <input asp-for="@Model.UserName" type="hidden" /> <table class="table table-striped"> <thead> <tr> <th> Role </th> <th> Description </th> <th> Status </th> </tr> </thead> <tbody> @for (var i = 0; i < Model.ManageRolesViewModel.Count(); i++) { <tr> <td> <input asp-for="@Model.ManageRolesViewModel[i].Name" type="hidden" /> @Model.ManageRolesViewModel[i].Name </td> <td> <input asp-for="@Model.ManageRolesViewModel[i].Description" type="hidden" /> @Model.ManageRolesViewModel[i].Description </td> <td> <div class="form-check m-1"> <input asp-for="@Model.ManageRolesViewModel[i].Checked" class="form-check-input" /> </div> </td> </tr> } </tbody> </table> </div> <div class="card-footer clearfix"> <div class="col-sm-12" style=" padding: 20px 20px 0px 0px"> <button type="submit" id="save" class="btn bg-primary"> <i class="fa fa-check"></i> Save </button> </div> </div> </form> </div>
Now add [AllowAnonymous] to methods where authorize attribute is not given yet in UserController, HomeController. Look HomeController code from here to understand more.
We are done for today. We learned today, Implementing PolicyBasedAuthorization, PaginatedList, Managing User Roles.
In next part, we will learn managing User Permissions & Role Permissions.