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)
- Policy Based Authorization, PaginatedList, Managing user roles (Part 04)
Today we will learn, managing User Permissions & Role Permissions. Assigning/De-assigning module permissions to users and to roles. Once a person has a specific permission, the user will get to to access all the endpoints/urls that require that permission.
We will need 7 more view models for this part. Lets create them under ViewModels folder one by one.
ManageUserClaimViewModel.cs
using System.ComponentModel.DataAnnotations; namespace Identity.ViewModels { public class ManageUserClaimViewModel { [Required] public string UserId { get; set; } [Required] public string UserName { get; set; } public string Type { get; set; } public string Value { get; set; } public bool Checked { get; set; } } }
ManageUserPermissionsViewModel.cs
using Identity.Helpers; namespace Identity.ViewModels { public class ManageUserPermissionsViewModel { public string UserId { get; set; } public string UserName { get; set; } public string PermissionValue { get; set; } public PaginatedList<ManageUserClaimViewModel> ManagePermissionsViewModel { get; set; } } }
AddRoleViewModel.cs
using System.ComponentModel.DataAnnotations; namespace Identity.ViewModels { public class AddRoleViewModel { [Required] public string Name { get; set; } public string Description { get; set; } } }
ManageClaimViewModel.cs
namespace Identity.ViewModels { public class ManageClaimViewModel { public ManageClaimViewModel() { } public ManageClaimViewModel(string type, string value, bool @checked) { this.Type = type; this.Value = value; this.Checked = @checked; } public string Type { get; set; } public string Value { get; set; } public bool Checked { get; set; } } }
ManageRoleClaimViewModel.cs
using System.ComponentModel.DataAnnotations; namespace Identity.ViewModels { public class ManageRoleClaimViewModel { [Required] public string RoleId { get; set; } [Required] public string RoleName { get; set; } public string Type { get; set; } public string Value { get; set; } public bool Checked { get; set; } } }
ManageRolePermissionsViewModel.cs
using Identity.Helpers; using System.ComponentModel.DataAnnotations; namespace Identity.ViewModels { public class ManageRolePermissionsViewModel { [Required] public string RoleId { get; set; } public string RoleName { get; set; } public string PermissionValue { get; set; } public PaginatedList<ManageClaimViewModel> ManagePermissionsViewModel { get; set; } } }
RoleViewModel.cs
namespace Identity.ViewModels { public class RoleViewModel { public string Id { get; set; } public string Name { get; set; } public string Description { get; set; } } }
Lets finish user level permissions part. In UserController.cs add two action methods.
- ManagePermissions(string userId, string permissionValue, int? pageNumber, int? pageSize)
This is a GET method responsible to retrieve user specific permissions details. Like permissions the user has and doesn’t. From this page, permission can be assign/de-assign through clicking checked mark. Clicking will send a ajax request to backend and will hit to ManageClaims(ManageUserClaimViewModel manageUserClaimViewModel) method.
- ManageClaims(ManageUserClaimViewModel manageUserClaimViewModel)
When check mark is clicked based on check/uncheck, this method will receive a request. And will do the assigning/de-assigning.
ManagePermissions(string userId, string permissionValue, int? pageNumber, int? pageSize)
[HttpGet] [Authorize(Policy = Permissions.Permissions.Users.ManageClaims)] public async Task<IActionResult> ManagePermissions(string userId, string permissionValue, int? pageNumber, int? pageSize) { var user = await _userManager.FindByIdAsync(userId); if (user == null) return RedirectToAction("Index"); var userPermissions = await _userManager.GetClaimsAsync(user); var allPermissions = PermissionHelper.GetAllPermissions(); if (!string.IsNullOrWhiteSpace(permissionValue)) { allPermissions = allPermissions.Where(x => x.Value.ToLower().Contains(permissionValue.Trim().ToLower())) .ToList(); } var managePermissionsClaim = new List<ManageUserClaimViewModel>(); foreach (var permission in allPermissions) { var managePermissionClaim = new ManageUserClaimViewModel { Type = permission.Type, Value = permission.Value }; if (userPermissions.Any(x => x.Value == permission.Value)) { managePermissionClaim.Checked = true; } managePermissionsClaim.Add(managePermissionClaim); } var paginatedList = PaginatedList<ManageUserClaimViewModel>.CreateFromLinqQueryable(managePermissionsClaim.AsQueryable(), pageNumber ?? 1, pageSize ?? 12); var manageUserPermissionsViewModel = new ManageUserPermissionsViewModel { UserId = userId, UserName = user.UserName, PermissionValue = permissionValue, ManagePermissionsViewModel = paginatedList }; return View(manageUserPermissionsViewModel); }
ManageClaims(ManageUserClaimViewModel manageUserClaimViewModel)
[HttpPost] [Authorize(Policy = Permissions.Permissions.Users.ManageClaims)] public async Task<IActionResult> ManageClaims(ManageUserClaimViewModel manageUserClaimViewModel) { var userById = await _userManager.FindByIdAsync(manageUserClaimViewModel.UserId); var userByName = await _userManager.FindByNameAsync(manageUserClaimViewModel.UserName); if (userById != userByName) return Json(new { Succeeded = false, Message = "Fail" }); var allClaims = await _userManager.GetClaimsAsync(userById); var claimExists = allClaims.Where(x => x.Type == manageUserClaimViewModel.Type && x.Value == manageUserClaimViewModel.Value).ToList(); switch (manageUserClaimViewModel.Checked) { case true when claimExists.Count == 0: { await _userManager.AddClaimAsync(userById, new Claim(manageUserClaimViewModel.Type, manageUserClaimViewModel.Value)); break; } case false when claimExists.Count > 0: { await _userManager.RemoveClaimsAsync(userById, claimExists); break; } } return Json(new {Succeeded = true, Message="Success"}); }
Lets create view file for ManagePermissions action method. Under Views/User, create razor page and name it ManagePermissions.cshtml . The content of this file will be
@model Identity.ViewModels.ManageUserPermissionsViewModel @{ ViewData["Title"] = "User Permissions"; Layout = "~/Views/Shared/_Layout.cshtml"; } <div class="card"> <div class="card-header"> <form asp-action="ManagePermissions" method="get"> <h3 class="card-title">Manage Permissions for User: <b>@Model.UserName</b></h3> <div class="float-right"> <p> Find by name: <input type="text" name="permissionValue" value="@Model.PermissionValue" /> <input type="submit" value="Search" class="btn btn-default" /> | <input asp-for="UserId" type="hidden" /> <a asp-action="ManagePermissions" asp-route-userId="@Model.UserId">Back to Full List</a> </p> </div> </form> </div> <div class="card-body"> <table class="table table-striped"> <thead> <tr> <th> Type </th> <th> Permission Name </th> <th> Status </th> </tr> </thead> <tbody> @for (var i = 0; i < Model.ManagePermissionsViewModel.Count(); i++) { <tr> <td class="text-center" id="claimType@(i)"> @Model.ManagePermissionsViewModel[i].Type </td> <td class="text-center" id="claimValue@(i)"> @Model.ManagePermissionsViewModel[i].Value </td> <td> <div class="form-check m-1"> <input asp-for="@Model.ManagePermissionsViewModel[i].Checked" class="form-check-input" id="checked@(i)" onchange="permissionChanged(@i)" /> </div> </td> </tr> } </tbody> </table> </div> <div class="card-footer clearfix"> @{ var prevDisabled = !Model.ManagePermissionsViewModel.HasPreviousPage ? "disabled" : ""; var nextDisabled = !Model.ManagePermissionsViewModel.HasNextPage ? "disabled" : ""; } <div class="row"> <div class="col-sm-9"> <ul class="pagination"> <li> <a asp-action="ManagePermissions" asp-route-userId="@Model.UserId" asp-route-pageNumber="@(Model.ManagePermissionsViewModel.PageIndex - 1)" class="btn btn-default @prevDisabled"> Previous </a> </li> @for (var i = 1; i <= Model.ManagePermissionsViewModel.TotalPages; i++) { <li class="page-item @(i == Model.ManagePermissionsViewModel.PageIndex ? "active" : "")" style="z-index: 0;"> <a asp-action="ManagePermissions" asp-route-userId="@Model.UserId" asp-route-pageNumber="@i" class="page-link">@i</a> </li> } <li> <a asp-action="ManagePermissions" asp-route-userId="@Model.UserId" asp-route-pageNumber="@(Model.ManagePermissionsViewModel.PageIndex + 1)" class="btn btn-default @nextDisabled"> Next </a> </li> </ul> </div> </div> </div> </div> @section Scripts { <script> var userId = '@Model.UserId'; var userName = '@Model.UserName'; function permissionChanged(i) { $.ajax( { type: "POST", url: "/User/ManageClaims", data: { UserId: userId, UserName: userName, Type: document.getElementById("claimType" + i).innerHTML.trim(), Value: document.getElementById("claimValue" + i).innerHTML.trim(), Checked: document.getElementById("checked" + i).checked }, success: function (response) { if (response != null) { if (response.succeeded === true) { Swal.fire({ title: 'Succeeded', text: response.message, icon: 'success' }); } else { Swal.fire({ title: 'Failed', text: response.message, icon: 'error' }); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; } } else { Swal.fire({ title: 'Failed', text: "Something went wrong", icon: 'error' }); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; } }, failure: function(response) { alert(response.responseText); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; }, error: function(response) { alert(response.responseText); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; } }); } </script> }
This page requires JavaScript SweetAlert2 library for pop dialogues. To make the functionality works, add the following line inside Views/Shared/_Layout.cshtml at the bottom just before the body tag.
<script src="//cdn.jsdelivr.net/npm/sweetalert2@10"></script>
Look this file if you do not understand where to place the line. Now go to Views/User/Index.cshtml, and uncomment this line @*<a asp-action="ManagePermissions" asp-route-userId="@item.Id">Manage Permissions</a>*@
User level permission part is done.
Role Level Permissions
We will implement Adding role, Managing permissions for role. Like adding permissions to existing role, removing permissions from existing roles. Under Controllers folder, create a new controller and name it RoleController.cs . It will have following methods.
- RoleController(RoleManager<AppRole> roleManager)
Constructor. Responsible to load required dependency.
- Index(int? pageNumber, int? pageSize)
This action method renders all roles into view page. From the page user can see all roles, add role button, manage permissions button. So, it has respective razor page.
- Add()
This action method act as GET request and renders page that has a form to add new role.
- Add(AddRoleViewModel addRoleViewModel)
POST method. Once user submit to create new role, this method accept that and do the functionalities.
- ManageRolePermissions(string roleId, string permissionValue, int? pageNumber, int? pageSize)
GET Method. This action method genere pages that shows permissions the role has, dosn’t has. Every permission has a check box. If user tries to change the status of check box like mark/unmark, a ajax request will be sent to ManageRoleClaims method described below.
- ManageRoleClaims(ManageRoleClaimViewModel manageRoleClaimViewModel)
This is a POST method and receive ajax request from ManageRolePermissions page. It does the work to assign/de-assign permission to given role.
The whole code of RoleController is given below
RoleController.cs
using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Identity.Helpers; using Identity.Models; using Identity.Permissions; using Identity.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace Identity.Controllers { public class RoleController : Controller { private readonly RoleManager<AppRole> _roleManager; public RoleController(RoleManager<AppRole> roleManager) { _roleManager = roleManager; } [HttpGet] [Authorize(Policy = Permissions.Permissions.Roles.View)] public async Task<IActionResult> Index(int? pageNumber, int? pageSize) { var roles = _roleManager.Roles; var rs = await PaginatedList<AppRole>.CreateFromEfQueryableAsync(roles.AsNoTracking(), pageNumber ?? 1, pageSize ?? 12); var rolesViewModel = rs.Select(role => new RoleViewModel {Name = role.Name, Description = role.Description, Id = role.Id}).ToList(); var response = new PaginatedList<RoleViewModel>(rolesViewModel, rs.Count, pageNumber ?? 1, pageSize ?? 12); return View(response); } [HttpGet] [Authorize(Policy = Permissions.Permissions.Roles.Create)] public IActionResult Add() { return View(new AddRoleViewModel()); } [HttpPost] [Authorize(Policy = Permissions.Permissions.Roles.Create)] public async Task<IActionResult> Add(AddRoleViewModel addRoleViewModel) { if (!ModelState.IsValid) return View(addRoleViewModel); if (await _roleManager.FindByNameAsync(addRoleViewModel.Name) != null) { ModelState.AddModelError(string.Empty, "The role already exists. Please try a different one!"); return View(addRoleViewModel); } var appRole = new AppRole(addRoleViewModel.Name) { Description = addRoleViewModel.Description}; var rs = await _roleManager.CreateAsync(appRole); if (rs.Succeeded) return RedirectToAction("Index", "Role", new {id = appRole.Id, succeeded = rs.Succeeded, message = rs.ToString()}); ModelState.AddModelError(string.Empty, rs.ToString()); return View(addRoleViewModel); } [HttpGet] [Authorize(Policy = Permissions.Permissions.Roles.ManageClaims)] public async Task<IActionResult> ManageRolePermissions(string roleId, string permissionValue, int? pageNumber, int? pageSize) { var role = await _roleManager.FindByIdAsync(roleId); if (role == null) return RedirectToAction("Index"); var roleClaims = await _roleManager.GetClaimsAsync(role); var allPermissions = PermissionHelper.GetAllPermissions(); if (!string.IsNullOrWhiteSpace(permissionValue)) { allPermissions = allPermissions.Where(x => x.Value.ToLower().Contains(permissionValue.Trim().ToLower())).ToList(); } var managePermissionsClaim = new List<ManageClaimViewModel>(); foreach (var permission in allPermissions) { var managePermissionClaim = new ManageClaimViewModel { Type = permission.Type, Value = permission.Value}; if (roleClaims.Any(x => x.Value == permission.Value)) { managePermissionClaim.Checked = true; } managePermissionsClaim.Add(managePermissionClaim); } var paginatedList = PaginatedList<ManageClaimViewModel>.CreateFromLinqQueryable(managePermissionsClaim.AsQueryable(), pageNumber ?? 1, pageSize ?? 12); var manageRolePermissionsViewModel = new ManageRolePermissionsViewModel { RoleId = roleId, RoleName = role.Name, PermissionValue = permissionValue, ManagePermissionsViewModel = paginatedList }; if( allPermissions.Count > 0) return View(manageRolePermissionsViewModel); return RedirectToAction("Index", "Role", new {succeeded = false, message = "No Permissions exists"}); } [HttpPost] [Authorize(Policy = Permissions.Permissions.Roles.ManageClaims)] public async Task<IActionResult> ManageRoleClaims(ManageRoleClaimViewModel manageRoleClaimViewModel) { if (!ModelState.IsValid) return Json(new {Succeeded =false, Messaege="failed"}); var roleById = await _roleManager.FindByIdAsync(manageRoleClaimViewModel.RoleId); var roleByName = await _roleManager.FindByNameAsync(manageRoleClaimViewModel.RoleName); if(roleById != roleByName) return Json(new { Succeeded = false, Messaege = "failed" }); var allClaims = await _roleManager.GetClaimsAsync(roleById); var claimExists = allClaims.Where(x => x.Type == manageRoleClaimViewModel.Type && x.Value == manageRoleClaimViewModel.Value).ToList(); switch (manageRoleClaimViewModel.Checked) { case true when claimExists.Count == 0: await _roleManager.AddClaimAsync(roleById, new Claim(manageRoleClaimViewModel.Type, manageRoleClaimViewModel.Value)); break; case false when claimExists.Count > 0: { foreach (var claim in claimExists) { await _roleManager.RemoveClaimAsync(roleById, claim); } break; } } return Json(new {Succeeded = true, Message="Success"}); } } }
We need to create three razor pages for RoleController.cs and they are Add.cshtml, Index.cshtml, ManageRolePermissions.cshtml. Under Views, create a folder Role. Inside this Role folder, create those razor pages and paste the following codes respectively.
Add.cshtml
@model Identity.ViewModels.AddRoleViewModel @{ ViewData["Title"] = "Add"; Layout = "~/Views/Shared/_Layout.cshtml"; } <div class="row"> <div class="col-2"></div> <div class="col-6"> <div class="card"> <div class="card-header"> <h3 class="card-title">Add New Role</h3> </div> <div class="card-body"> <form asp-action="Add"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Name" class="control-label"></label> <input asp-for="Name" class="form-control" /> <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Description" class="control-label"></label> <input asp-for="Description" class="form-control" /> <span asp-validation-for="Description" class="text-danger"></span> </div> <div class="form-group"> <input type="submit" value="Create" class="btn btn-primary" /> </div> </form> </div> <div class="card-footer clearfix"> <a asp-action="Index">Back to List</a> </div> </div> </div> <div class="col-8"></div> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
Index.cshtml
@model Identity.Helpers.PaginatedList<Identity.ViewModels.RoleViewModel> @{ ViewData["Title"] = "View Roles"; Layout = "~/Views/Shared/_Layout.cshtml"; } <div class="card"> <div class="card-header"> <h3 class="card-title">Roles</h3> <a asp-action="Add" class="float-right">Add New Role</a> </div> <div class="card-body"> <table class="table table-bordered table-hover"> <thead> <tr> <th> Id </th> <th> Name </th> <th> Description </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.Name) </td> <td> @Html.DisplayFor(modelItem => item.Description) </td> <td> <a asp-action="ManageRolePermissions" asp-route-roleId="@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>
ManageRolePermissions.cshtml
@model Identity.ViewModels.ManageRolePermissionsViewModel @{ ViewData["Title"] = "Role Permissions"; Layout = "~/Views/Shared/_Layout.cshtml"; } <div class="card"> <div class="card-header"> <form asp-action="ManageRolePermissions" method="get"> <h3 class="card-title">Manage Permissions for Role: <b>@Model.RoleName</b></h3> <div class="float-right"> <p> Find by name: <input type="text" name="permissionValue" value="@Model.PermissionValue" /> <input type="submit" value="Search" class="btn btn-default" /> | <input asp-for="RoleId" type="hidden" /> <a asp-action="ManageRolePermissions" asp-route-roleId="@Model.RoleId">Back to Full List</a> </p> </div> </form> </div> <div class="card-body"> <table class="table table-striped"> <thead> <tr> <th> Type </th> <th> Permission Name </th> <th> Status </th> </tr> </thead> <tbody> @for (var i = 0; i < Model.ManagePermissionsViewModel.Count(); i++) { <tr> <td class="text-center" id="claimType@(i)"> @Model.ManagePermissionsViewModel[i].Type </td> <td class="text-center" id="claimValue@(i)"> @Model.ManagePermissionsViewModel[i].Value </td> <td> <div class="form-check m-1"> <input asp-for="@Model.ManagePermissionsViewModel[i].Checked" class="form-check-input" id="checked@(i)" onchange="permissionChanged(@i)" /> </div> </td> </tr> } </tbody> </table> </div> <div class="card-footer clearfix"> @{ var prevDisabled = !Model.ManagePermissionsViewModel.HasPreviousPage ? "disabled" : ""; var nextDisabled = !Model.ManagePermissionsViewModel.HasNextPage ? "disabled" : ""; } <div class="row"> <div class="col-sm-9"> <ul class="pagination"> <li> <a asp-action="ManageRolePermissions" asp-route-roleId="@Model.RoleId" asp-route-pageNumber="@(Model.ManagePermissionsViewModel.PageIndex - 1)" class="btn btn-default @prevDisabled"> Previous </a> </li> @for (var i = 1; i <= Model.ManagePermissionsViewModel.TotalPages; i++) { <li class="page-item @(i == Model.ManagePermissionsViewModel.PageIndex ? "active" : "")" style="z-index: 0;"> <a asp-action="ManageRolePermissions" asp-route-roleId="@Model.RoleId" asp-route-pageNumber="@i" class="page-link">@i</a> </li> } <li> <a asp-action="ManageRolePermissions" asp-route-roleId="@Model.RoleId" asp-route-pageNumber="@(Model.ManagePermissionsViewModel.PageIndex + 1)" class="btn btn-default @nextDisabled"> Next </a> </li> </ul> </div> </div> </div> </div> @section Scripts { <script> var roleId = '@Model.RoleId'; var roleName = '@Model.RoleName'; function permissionChanged(i) { $.ajax( { type: "POST", url: "/Role/ManageRoleClaims", data: { RoleId: roleId, RoleName: roleName, Type: document.getElementById("claimType" + i).innerHTML.trim(), Value: document.getElementById("claimValue" + i).innerHTML.trim(), Checked: document.getElementById("checked" + i).checked }, success: function(response) { if (response != null) { if (response.succeeded === true) { Swal.fire({ title: 'Succeeded', text: response.message, icon: 'success' }); } else { Swal.fire({ title: 'Failed', text: response.message, icon: 'error' }); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; } } else { Swal.fire({ title: 'Failed', text: "Something went wrong", icon: 'error' }); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; } }, failure: function(response) { alert(response.responseText); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; }, error: function(response) { alert(response.responseText); document.getElementById("checked" + i).checked = !document.getElementById("checked" + i).checked; } }); } </script> }
We have implemented user level permissions, role level permissions. You can see that I have used permission based authorization in all action methods of UserController.cs, RoleController.cs. I have shown you how it is done through this series. So, now you can bring it into your application, as the way you want. If need any helps, just give me a knock.
With this part, our Authentication & Permission based authorization implementations come to end. Source code of this part can be found here. Thanks for your patience troughout the series.