32
Access (Permissions) Control List in .NET C#
During my professional experience, I have had to implement functionality to limit certain users to resources or routes, based on the roles/permissions, that users have. So I decided to share my experience and develop a small library, through which anyone can easily integrate Access control, based on roles and permissions in his/her application.
The method I decided to use is inspired by a Nodejs based library, following ACL (Access control list) methodology mostly used for File System and Networking permissions.
An access control list (ACL) contains rules that grant or deny access to certain digital environments.
ACL is a table that informs a Computer operating system/Application of the access privileges a user has to a system object, resource, file or a file directory. Each object has a security property that connects it to its access control list. The list has an entry for every user with access rights to the system.
When a user requests an object in an ACL-based security model, the operating system or Application studies the ACL for a relevant entry and sees whether the requested operation is permissible.
You can read more in reference links
The standard scenario in which I use the library is when developing Backend Services, but it can also be used in any type of application that needs additional protection.
Specifically in Backend services, I use it, routing endpoints in a format that allows me to determine the resource and permissions for each one:
For example, the permission is determined by HTTP method, and the resource of the first word in the route.
GET /{resource}/something-else
GET is mapped as “view”, POST as “create”, PUT and Update as “update” and DELETE as ‘delete’
PUT /users/{userId}
So in this case the user should have role, that allow him to operate with resource Users and has permission Update, in order to execute this end-point.
First, we need to download and/or install the Acl.Net library. You can find it in Github/Acl.Net or in NuGet, as it is under a MIT license and is completely free and available for use.
Now the fun part begins:
Currently the library provides 3 options for persist the the state. As in the future, perhaps, there will be additional saving options, as well as an option for your own extension:
- InMemory: Provides in memory persistence, which means that information will be kept as long as the application is alive.
- Redis: Provides persistence into redis database.
- Mongodb: Provides persistence into mongodb database.
For additional information and how to use click here.
In this example we will define, a mongodb Backend and pass it to Acl controller. After that the Role for “Administrator” is created (Allowed) for “User” and “Devices” resources with permissions for usage and assigned (added) to user “YourUser” (The identifier can be string or integer). After that, there is a series of checks, if user has permission/s for target resource.
using Acl.Net.Backends;
using Acl.Net.Entities;
using Acl.Net.Interfaces;
using MongoDB.Driver;
namespace ConsoleApp
{
internal class Program
{
static void Main()
{
string mongoUrl = "mongodb://localhost:27017/Acl";
string databaseName = "Acl";
MongoClient db = new MongoClient(mongoUrl);
IBackend backend = new Mongodb(db, databaseName);
IAcl acl = new Acl.Net.Acl(backend);
acl.Allow(
new Role()
{
Name = "Administrator",
Resources = new[]
{
new Resource()
{
Name = "Users",
Permissions = new[] { "View", "Create", "Update", "Delete" }
},
new Resource()
{
Name = "Devices",
Permissions = new[] { "View", "Create", "Update" }
}
}
}
);
string username = "YourUser";
acl.AddUserRole(username, "Administrator");
bool isAllowedToViewAndCreateUsers = acl.IsAllowed(username, new Resource() { Name = "Users", Permissions = new[] { "View", "Create" } });
// Result: True
bool isAllowedDeleteUsers = acl.IsAllowed(username, new Resource() { Name = "Users", Permissions = new[] { "Delete" } });
// Result: True
bool isAllowedDeleteDevices = acl.IsAllowed(username, "Devices", "Delete");
// Result: False
bool isAllowedViewDevices = acl.IsAllowed(username, "Devices", "View");
// Result: True
}
}
}
The idea here is same as the example on top. We can have a middleware, which will check if requesting user has a permission to requested resource. This middleware will be executed before controller and prevent logical execution, if user is not allowed to this resource.
The user identifier can be fetched by decrypt JWT token, passed through Authentication header or other way application marks the authenticated user.
The example below shows one of the possible ways to use as middleware:
using Acl.Net.Backends;
using Acl.Net.Interfaces;
using Microsoft.AspNetCore.Http;
using MongoDB.Driver;
using System.Net;
using System.Threading.Tasks;
namespace WebApplication
{
public class PermissingGuardMiddleware
{
private readonly RequestDelegate _next;
private readonly Acl.Net.Acl _acl;
public PermissingGuardMiddleware(RequestDelegate next)
{
string mongoUrl = "mongodb://localhost:27017/Acl";
string databaseName = "Acl";
MongoClient db = new MongoClient(mongoUrl);
IBackend backend = new Mongodb(db, databaseName);
this._acl = new Acl.Net.Acl(backend);
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
string authHeader = context.Request.Headers["Authentication"].ToString();
string[] paths = ((string)context.Request.Path).Split('/');
string permission = TransforMethodToPermission(context.Request.Method);
if (permission == null || paths[0] == null)
{
await Unauthorize(context);
return;
}
try
{
bool isAllowed = this._acl.IsAllowed(authHeader, paths[0], permission);
if (!isAllowed)
{
await Unauthorize(context);
return;
}
await _next(context);
}
catch (System.Exception)
{
await Unauthorize(context);
return;
}
}
private string TransforMethodToPermission(string method)
{
switch (method.ToUpper())
{
case "POST":
return "Creare";
case "GET":
return "View";
case "DELETE":
return "Delete";
case "PUT":
case "UPDATE":
return "Update";
default:
return null;
}
}
private async Task Unauthorize(HttpContext context)
{
context.Response.Clear();
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Unauthorized");
}
}
}
Additional information about the library, its methods and possibilities can be found here:
That’s it, hope you found reading this story as interesting as I found writing it.
I also hope I have been helpful to some of you who have encountered a similar problem and are looking for a solution.
32