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 policy attribute added to it that will act as the Permission that will permit access to that feature that the controller routes you to. Because we're using HeimGuard, this permission will automatically be registered for us.
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.
The above permissions will permit access to a feature, but what if there are specific business rules that need to be enforced? For example, even if a user as access to a feature, they may only be allowed to do that action if certain criteria are met. This is a separate concern than feature access. It is recommended to build that into your CQRS handlers.
If you have multiple permissions that can access a feature, then you'll want to register a new policy for it in your UseAuthorization
registration that
captures both permissions:
options.AddPolicy("ThisThingOrThatThing", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == "ThisThing" ||
c.Type == "ThatThing"))));
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.
By default, there is a SuperAdmin
role and a User
role generated for you where a SuperAdmin
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 aSuperAdmin
andBob
is aUser
.
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
SuperAdmin
can, by default, do everything.
There are tests generated for all of the auth features for you so you can safely make changes.