Permissions

Permissions is another KIT concept which is essential for building your modules. We will define all of our permissions inside an C# enum.

For the purpose of this example we will add a permission for who can view, add and update products and a separate one for who has the ability to delete products.

First add a file called "Permission.cs" to the root of your module with the following content:

namespace DemoShop.Shop;

public enum Permission {
    Products,
    ProductsDelete
}

Now we have to add a migration to add our permissions to the database. Add a file called "Migration6.cs" to the "Migrations" folder with the following content:

using FluentMigrator;
using Kit.Security;

namespace DemoShop.Shop.Migrations;

[Migration(201908141828)]
public class Migration6 : AutoReversingMigration {
    public override void Up() {
        Insert.IntoTable("Permissions")
            .Row(new { GroupId = (int)PermissionGroup.Content, Name = "Products", MinLevel = 3, Namespace = "DemoShop.Shop", ConstantName = "Products" })
            .Row(new { GroupId = (int)PermissionGroup.Content, Name = "Products: Delete", MinLevel = 3, Namespace = "DemoShop.Shop", ConstantName = "ProductsDelete" });

        Insert.IntoTable("RolePermissions")
            .Row(new { RoleId = 1, PermissionId = RawSql.Insert("(SELECT [Id] FROM [dbo].[Permissions] WHERE [Namespace] = 'DemoShop.Shop' AND [ConstantName] = 'Products')") })
            .Row(new { RoleId = 2, PermissionId = RawSql.Insert("(SELECT [Id] FROM [dbo].[Permissions] WHERE [Namespace] = 'DemoShop.Shop' AND [ConstantName] = 'Products')") })
            .Row(new { RoleId = 1, PermissionId = RawSql.Insert("(SELECT [Id] FROM [dbo].[Permissions] WHERE [Namespace] = 'DemoShop.Shop' AND [ConstantName] = 'ProductsDelete')") })
            .Row(new { RoleId = 2, PermissionId = RawSql.Insert("(SELECT [Id] FROM [dbo].[Permissions] WHERE [Namespace] = 'DemoShop.Shop' AND [ConstantName] = 'ProductsDelete')") });
    }
}

Note: Not only do we add the permissions, but we add the role permissions so that both "Super Admins" (RoleId = 1) and "Admins" (RoleId = 2) have permission for each one.

Now return back to the "AdminController.cs" file and decorate all of our action methods with the following attribute (adding any missing namespaces):

[HasPermission(Permission.Products)]

Any action methods with this attribute will only be accessible by someone who has the required permission, otherwise an authentication error is thrown.

Next we will lock the ability to delete products down to people with appropriate permission.

First we will only show the delete option in our view if they have permission to see it. Open the "Products.cshtml" view and replace the delete option (in the icons list) with the following:

if (Authorization.IsAllowed((await Context.GetUserAsync())!, DemoShop.Shop.Permission.ProductsDelete)) {
    <li class="delete"><button type="button" data-bs-toggle="modal" data-bs-target="#confirm-delete-modal">Delete</button></li>
}

Note: The call to “(await Context.GetUserAsync())!”, this will get the current logged in user. This will return null if the current user is logged out, however since we already locked this page down (using the attribute above) we know they will be logged in. Therefore we can add the ! null-forgiving operator as we know the method won't return null.

This will show an error message since the view has no knowledge of the "Authorization.IsAllowed" method. To allow this we have to add the following before the first line of the view:

@inject Kit.Security.Services.IAuthorizationService Authorization

This will inject the authorization service directly into our views.

Whilst we have hidden the link to delete a product, the user could still tamper with the page to add it back and fire a form submission with the "Delete" command to the server. Therefore we will go back to the "AdminController.cs" file and add the following permission check before we try to delete the product:

// Make sure they are allowed.
if (!_authorizationService.IsAllowed((await HttpContext.GetUserAsync())!, Permission.ProductsDelete))
    return Unauthorized();

Once again, you will need to inject the authorization service and add any missing namespaces. The final result of the "AdminController.cs" file should be:

using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using DemoShop.Shop.Models;
using DemoShop.Shop.ViewModels.Admin;
using Kit.Data;
using Kit.Security;
using Kit.Security.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace DemoShop.Shop.Controllers;

[Area("DemoShop.Shop"), Route("admin")]
public class AdminController : Controller {
    private readonly IAuthorizationService _authorizationService;
    private readonly IDataContext _dataContext;

    public AdminController(IAuthorizationService authorizationService, IDataContext dataContext) {
        _authorizationService = authorizationService;
        _dataContext = dataContext;
    }

    [HasPermission(Permission.Products), HttpGet("products")]
    public async Task<IActionResult> Products(int page = 1, int pageSize = 10, string? sortBy = null, ListSortDirection? sortDirection = null) {
        var timeZone = await HttpContext.GetTimeZoneAsync();

        return View((await _dataContext.Repository<Product>().All().ToSortedPagedListAsync(page, pageSize, sortBy ?? "Name", sortDirection ?? ListSortDirection.Ascending)).Convert(p => new ProductViewModel(p, timeZone)));
    }

    [HasPermission(Permission.Products), HttpPost("products"), ValidateAntiForgeryToken]
    public async Task<IActionResult> Products(string command, int? id, int page = 1, int pageSize = 10, string? sortBy = null, ListSortDirection? sortDirection = null) {
        if (id.HasValue) {
            switch (command) {
                case "Delete":
                    try {
                        // Make sure they are allowed.
                        if (!_authorizationService.IsAllowed((await HttpContext.GetUserAsync())!, Permission.ProductsDelete))
                            return Unauthorized();

                        // Try to delete the product.
                        await _dataContext.Repository<Product>().DeleteAsync((await _dataContext.Repository<Product>().GetAsync(id))!);
                        await _dataContext.CommitAsync();
                    } catch {
                        // Add the error.
                        ModelState.AddModelError("Error", "Product cannot be deleted.");

                        // Rollback the changes.
                        await _dataContext.RollbackAsync();

                        return await Products(page, pageSize, sortBy, sortDirection);
                    }

                    break;
                case "Edit":
                    return RedirectToAction(nameof(EditProduct), new { id, returnUrl = Url.Action(nameof(Products), new { page, pageSize, sortBy, sortDirection }) });
            }
        }

        return RedirectToAction(nameof(Products), new { page = 1, pageSize, sortBy, sortDirection });
    }

    [HasPermission(Permission.Products), HttpGet("products/new")]
    public IActionResult NewProduct() {
        return View(new ProductFormViewModel(new Product()));
    }

    [HasPermission(Permission.Products), HttpPost("products/new"), ValidateAntiForgeryToken]
    public async Task<IActionResult> NewProduct(ProductFormViewModel model) {
        // Validate the form.
        if (!ModelState.IsValid)
            return View(model.Initialize(new Product()));

        // Insert the product.
        await _dataContext.Repository<Product>().InsertOrUpdateAsync(model.ToModel());
        await _dataContext.CommitAsync();

        return RedirectToAction(nameof(Products));
    }

    [HasPermission(Permission.Products), HttpGet("products/{id:int}/edit")]
    public async Task<IActionResult> EditProduct(int id) {
        return View(new ProductFormViewModel((await _dataContext.Repository<Product>().GetAsync(id))!));
    }

    [HasPermission(Permission.Products), HttpPost("products/{id:int}/edit"), ValidateAntiForgeryToken]
    public async Task<IActionResult> EditProduct(int id, ProductFormViewModel model, string? returnUrl = null) {
        var product = (await _dataContext.Repository<Product>().GetAsync(id))!;

        // Validate the form.
        if (!ModelState.IsValid)
            return View(model.Initialize(product));

        // Update the product.
        model.ToModel(product);
        await _dataContext.CommitAsync();

        if (Url.IsLocalUrl(returnUrl))
            return Redirect(returnUrl);
        else
            return RedirectToAction(nameof(Products));
    }
}

Resources »