Authorization
ASP.NET Core has a built-in mechanism of authorization based on roles, claims and policies. Bulletcode.NET makes it easier to use by implementing an N-to-N mapping between roles and policies.
Instead of restricting the access to a particular controller or action to a hardcoded role or list or roles, it can be restricted to a specific policy. The application can then define roles which can contain any number of policies, and can inherit permissions from other roles. This makes it possible to introduce new roles or modify the permissions of existing roles, without having to update the permission checks across the application.
Types of policies
There are two types of policies in Bulletcode.NET:
- Global policies are high-level permissions assigned to certain roles. For example, a
"ManageUsers"
policy can enable access to the user management section of the administration panel, and theViewEvents
policy can enable access to the event log. - Conditional policies are low-level permissions to perform operations on specific target objects. For example, a
"DeleteUser"
policy can allow an administrator to delete another user, but disallow administrators from deleting themselves.
Global policies can be used to restrict access to controllers or action using the Authorize
attribute, for example:
[Authorize( "ManageUsers" )]
public class UsersController : BaseController
{
}
To evaluate conditional policies, use the AuthorizeAsync()
method of the IAuthorizationService
, passing the claims principal object, the target object and the name of the policy, for example:
if ( !( await _authorizationService.AuthorizeAsync( User, user, "DeleteUser" ) ).Succeeded )
return Forbid();
Configuring roles
In order to enable the role-based authorization mechanism, call the AddRoleBasedAuthorization()
extension method of IServiceCollection
and pass a callback function which configures the roles, for example:
services.AddRoleBasedAuthorization( options => {
options.AddRole( "admin", builder => builder
.AddPolicy( "ManageUsers" )
.AddPolicy( "DeleteUser", new OtherUserRequirement() )
.AddPolicy( "ToggleUser", new OtherUserRequirement() )
.AddPolicy( "ViewEvents" )
);
} );
You can pass one or more requirement object to the AddPolicy()
method. When multiple requirements are passed, all must be fulfilled in order for the authorization to be successful.
Custom requirements
In order to implement a custom requirement, implement both the IAuthorizationHandler
and IAuthorizationRequirement
interfaces in the same class, for example:
public class ExampleRequirement : AuthorizationHandler<ExampleRequirement, ExampleEntity>,
IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ExampleRequirement requirement,
ExampleEntity resource
)
{
int? userId = context.User.FindUserId();
if ( userId != null && resource.OwnerId == userId )
context.Succeed( requirement );
return Task.CompletedTask;
}
}
The example requirement can be used to evaluate a conditional policy for target objects of the ExampleEntity
class. In this case, the requirement is fulfilled when the OwnerId
property of the target object is the same and the ID of the current user.
Inherited roles
A role can inherit policies from one or more parent roles:
services.AddRoleBasedAuthorization( options => {
options.AddRole( "admin", builder => builder
.AddInheritedRole( "accountant" )
.AddInheritedRole( "manager" )
);
} );
In this case, the "admin"
role has the same policies as the "accountant"
and "manager"
roles combined.
A role can contain the same policies as the parent role, but with different requirements. For example, the "accountant"
role may have a "EditExample"
policy with the ExampleRequirement
, and the "admin"
role may have the same policy without any requirements. In this case, both policies are evaluated, and if any of them succeeds, the AuthorizeAsync()
method also succeeds, which means that the "admin"
will always be allowed to perform this operation.
Services
The IUserPoliciesService
can be used to list all policies which are available for the specified claims principal. Note that in this case requirements are not evaluated, and all conditional policies are also included. Inherited policies are also included. This can be useful when implementing a REST API, so that the client application knows the user’s permissions.
NOTE
Internally, Bulletcode.NET uses a custom IAuthorizationPolicyProvider
to implement role-based authorization. Since the same authorization policy is always returned, regardless of the user, a placeholder requirement object is created, which contains the name of the policy.
A custom IAuthorizationHandlerContextFactory
service replaces this placeholder requirement with actual requirements based on the user’s role. If the role doesn’t contain this specific policy, the placeholder requirement is not replaced and it will not be fulfilled.