Permissions

This writeup describes how permissions are set up in Wrapt projects.

Introduction

.NET gives us a lot of awesome capabilities around permissions, but after a lot of research and usage, I've found some gaps that haven't been addressed yet either. This lead me to write a small permissions extension on top of .NETs built in architecture called HeimGuard which you can read more about here.

How Does Craftsman Scaffold Out Permissions?

As a base, Craftsman uses a HeimGuard configuration to manage permissions. The library is inherently flexible though, so how is it handled here?

Permissions

Permissions dictate whether or not something is accessible and are the heart of authorization for your features. Each controller will have a normal .NET Authorize attribute added to it that will act as an authentication guard, but that doesn't cover the actual permission that decides whether or not I'm allowed to use that resource. This authorization check actually lives in your features using HeimGuard to check a user's permissions. This was chosen to help facilitate business rules in one central location (a la vertical slice).

For example, a feature may expect that some users have a permission to get an entire dataset but others can only get a subset of it. For example:


    public class Handler : IRequestHandler<RecipeWorklistQuery, List<RecipeDto>>
    {
        private readonly IHeimGuardClient _heimGuard;

        public Handler(IHeimGuardClient heimGuard)
        {
            _heimGuard = heimGuard;
        }

        public async Task<List<RecipeDto>> Handle(RecipeWorklistQuery request, CancellationToken cancellationToken)
        {
            if (await _heimGuard.HasPermissionAsync(Permissions.CanReadAllRecipes))
            {
                // get all data
            }
            else if (await _heimGuard.HasPermissionAsync(Permissions.CanReadMyRecipes))
            {
                // get data for current user
            }
            else
            {
                throw new ForbiddenAccessException();
            }
        }
    }

Where do these Permissions live though? In a Wrapt project, they are added to your boundary's Domain directory as constants that can be easily accessed. Why not put them in a database? The biggest reason is that permissions are coupled directly with your code. If you are updating a your permission configuration and it lived in a db, you still have to update your code to make your attributes match. If you're worried about being able to get a list of all your permissions, just make an endpoint that returns the list.

If you don't like this you can absolutely change this to a different pattern, you just need to update the UserPolicyHandler and tests.

Roles

Roles are a way to group permissions together and easily assign them to a user. This means that roles have no DIRECT bearing on accessing a feature, they just allow us to grab permissions associated to that role so we can see what a user can do.

Roles are added to the SharedKernel (also as constants) so they can be easily accessed. Now in this case, there can definitely be cases where you would want to have roles sitting in a database so your users can manage whatever roles make sense for them. At the end of the day, in many situations you don't need to care if you have 2 roles or 2000 roles, as long as you can get the assigned permissions for them.

To do this, you'd likely want to make some kind of Administration boundary that can can be used to create roles, but there is some additional complexity that would come into play if you went down that road.

Role Based Access Control (RBAC)

It is strongly recommended that you use roles to group permissions instead of creating permissions that match role names. Roles are a great way to group permissions together, but the permission is what should be used to determine access, not the role. The role is just the collection of permissions that a user has.

Default Roles

By default, there is a Super Admin role and a User role generated for you where a Super Admin role has all permissions, and a User role has no permissions. This can be updated in the UserPolicyHandler if you'd like.

Default Username and Password

If you are scaffolding out an auth server using craftsman, Alice is a Super Admin and Bob is a User.

The password for alice is alice and the password for bob is bob. These are marked as temporary passwords and you will be prompted to change them on login.

Assigned Permissions To Roles

The mapping of permissions to roles is the root of your configuration and is done using a prebuilt Domain Entity called RolePermission.

No role-permission mappings are added here by default, so you'll need to add them yourself. Until you do so, a Super Admin can, by default, do everything.

Tests

There are tests generated for all of the auth features for you so you can safely make changes.

For integration tests, TestBase has a mock of IHeimGuardClient that will default to true for all permissions. If you want to test a specific flow you can do that in an integration test, but I'd recommend doing them in unit tests wherever possible and using integration tests for happy path tests as they are more expensive.