A small test project to play with Permission-based authorization in .NET 5 MVC. A combination of ideas and implementations which I have found in various articles and posts on the internet.
- Permissions working at a basic level. Can (un)assign permissions from roles, and assign roles to users.
- Working with Cookies
- Apply changes to user roles and role permissions immediately - don't require user to log out and back in for changes to be applied to cookie
- Working with JWT (Separate API endpoints)
- Override permissions on a per-user basis (?)
docker-compose up
in ./database
to spin up the Postgres database in a docker container. Uses a volume to persist data. Then, run the Application with dotnet run
. Go to https://localhost:5001/ to explore the app, and https://localhost:8080/ to explore the database in Adminer with login and pass "postgres".
- Authentication using ASP.NET Identity Users and Roles.
- Permission-based Authorization (via roles)
- Web-based management for creating, deleting and editing permissions of roles, as well as assigning users to roles.
Kept very simple as it is only a sample, and to be used for reference.
There are 3 main things which are required to set this up:
IAuthorizationRequirement
, in this casePermissionRequirement
. A basic class which contains data about the requirement (such as the name of the permission the User is required to have).IAuthorizationPolicyProvider
, in this casePermissionPolicyProvider
. The policy provider is used when processing theAuthorize
attribute on controllers and actions. We can implementGetPolicyAsync(string policyName)
to return a policy which includes anyIAuthorizationRequirement
's which we want to be fulfilled. In this example project, each "Permission" will have it's own policy. The policies will be dynamically generated at runtime, depending on the permission name passed into the authorize attribute, e.g.[Authorize(Permissions.Roles.Read)]
AuthorizationHandler<IAuthorizationRequirement>
, in this casePermissionAuthorizationHandler
. When you set the running policy has a requirement that the handler handles, theHandleRequirementAsync
method of this class will be called. This method can be overridden to include logic to check if the authenticated user (context.User
) fulfils the requirement.
There are a few helpful but non-essential classes that have been set up too.
HasPermissionAttribute
: A wrapper around theAuthorize
attribute, which is just for organisation and clarity of intent ([HasPermission(Permissions.Roles.Read)]
).Permissions
: A class which contains all permissions - example below. Avoids using raw strings around the codebase for permissions. This can have nested classes for better organisation, for example rather than doingPermissions.RolesRead
,Permissions.UsersEdit
, etc, the permissions can be put under a main "Roles" or "Users" class, which cleans up intellisense and makes things easier to read. I have seen this be made anenum
instead, which has the added benefit of each permission being a number, so it takes up less space when sent to the browser in a cookie. The class-based implementation could be converted to have the same benefits by changing it from strings to integers, and unit tests could be written to ensure there are no duplicate values.
public static class Permissions
{
public static class Users
{
[Display(GroupName = "Users", Name = "Create", Description = "Create new users.")]
public const string Create = "Permissions.Users.Create";
[Display(GroupName = "Users", Name = "Read", Description = "Read user data.")]
public const string Read = "Permissions.Users.Read";
[Display(GroupName = "Users", Name = "Edit", Description = "Edit user data.")]
public const string Edit = "Permissions.Users.Edit";
}
}