This writeup describes how permissions are set up in Wrapt projects.
.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.
As a base, Craftsman uses a HeimGuard configuration to manage permissions. The library is inherently flexible though, so how is it handled here?
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 want to make sure users are allowed to read recipes before they can access a worklist:
public class Handler(IHeimGuardClient heimGuard) : IRequestHandler<RecipeWorklistQuery, List<RecipeDto>>
{
public async Task<List<RecipeDto>> Handle(RecipeWorklistQuery request, CancellationToken cancellationToken)
{
await heimGuard.MustHavePermission<ForbiddenAccessException>(Permissions.CanReadRecipes);
// Do work
}
}
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 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.
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.
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.
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.
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.
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.