User permission¶
Authorization is used to check if a user is allowed to perform some specific operations in the PSS®X application.
ABP framework extends ASP.NET Core Authorization by adding permissions as auto policies and allowing authorization system to be usable in the application services too.
So, all the ASP.NET Core authorization features and the documentation are valid in an PSS®X application.
In order to understand this authorization concept correctly, it is necessary to have basic understanding at the following topics:
Permissions, Privileges, and Scopes¶
Access control is a complex thing. Three core concepts are usually misunderstood and confused: permissions
, privileges
, and scopes
.
Permissions¶
Usually, you have a clear understanding of what a permission is. Commonly, you say that you have the permission to drive your car.
A permission is a declaration of an action that can be executed on a resource. It describes intrinsic properties of resources, which exist independently of the user.
Privileges¶
If permissions are bound to the resource, how can you express the ability to perform an action on that resource? How can you express the fact that someone is authorized to drive their car? What you need are privileges.
Simply put, privileges are assigned permissions. When you assign a permission to a user, you are granting them a privilege. If you assign a user the permission to read a document, you are granting them the privilege to read that document.
Scopes¶
Scopes, in OAuth2, is one of the most confusing concepts.
Scopes enable a mechanism to define what an application can do on behalf of the user.
Scopes
are per client application, while permissions
are per user. In other words, one client application can have a scope(s) to access certain API(s), but the users of this client application will have different permissions in this api (based on their roles).
OAuth2 scopes are for restricting what an application can do with a user's resource, not for restricting what a user can do in the application. Yet some developers often fall into the trap of using scopes in every authorization scenario.
The root of the problem¶
Often implementing a simple differentiation between two types of users - "Admin"/"Private" and "Not-Admin"/"Public". Creating an OAuth2 scope to represent the "admin" permission is very simple. When an Admin user logs in, the authentication layer adds the admin scope into the JSON Web Token (JWT), and every call to a protected resource checks the JWT for this "admin" scope.
Why is this an issue?¶
In short, OAuth2 scopes are not a substitute for application-level permissions - Scopes define the level of access that a client has to a protected resource, but they do not provide the granularity necessary to define what the client can do with that resource. Here, the crucial difference between OAuth2 scopes and application-level permissions comes in.
We will just add more scopes¶
As your application develops, simple concepts such as "Admin"/"Private" and "Not-Admin"/"Public" become insufficient, as they quickly lead to over-permissioning, which can result in data breaches or unauthorized access to resources.
The amount of different roles, resources, and actions is likely to grow exponentially along with your application. The size of a JWT is limited by the maximum size of an HTTP header, which is typically 8KB.. If we need to give a user access to 1000 files, the JWT length changes from 239 characters to 20,057 (and that’s considering a simple file name). With the full path, it’s even longer.
OAuth2 scopes are satic¶
OAuth2 does not account for changes in access requirements over time. Once a client application is granted a scope, it retains that scope until it is revoked. This can become a problem when access requirements change and the same scope is no longer sufficient. For example, if an application adds a new feature that requires more granular access control, it may be challenging to implement this change without breaking backward compatibility. In such cases, developers may need to create new scopes, which can be confusing for users and may lead to scope proliferation.
OAuth2 scopes don’t consider context¶
Different resources in an application may require different levels of access depending on the context. For instance, a user may have read-only access to some resources but require write access to others. Scopes do not account for such context-specific access requirements, which can lead to over-privileging or under-privileging users.
OAuth2 scopes don’t support complex policies¶
Access control policies often involve complex logic, such as role-based access control (RBAC) or attribute-based access control (ABAC). OAuth2 scopes are not designed to support such complex policies, which can lead to significant security vulnerabilities
Authorization types¶
ASP.NET Core authorization provides a simple, declarative role and a rich policy-based model. Authorization is expressed in requirements, and handlers evaluate a user's claims against requirements. Imperative checks can be based on simple policies or policies which evaluate both the user identity and properties of the resource that the user is attempting to access.
- Role-based authorization in ASP.NET Core
- Claims-based authorization in ASP.NET Core
- Policy-based authorization in ASP.NET Core
IAuthorizationService¶
The primary service that determines if authorization is successful is IAuthorizationService:
The preceding code highlights the two methods of the IAuthorizationService.
IAuthorizationRequirement is a marker service with no methods, and the mechanism for tracking whether authorization is successful.
Each IAuthorizationHandler is responsible for checking if requirements are met:
/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
/// <summary>
/// Makes a decision if authorization is allowed.
/// </summary>
/// <param name="context">The authorization information.</param>
Task HandleAsync(AuthorizationHandlerContext context);
}
The AuthorizationHandlerContext class is what the handler uses to mark whether requirements have been met:
context.Succeed(requirement)
The following code shows default implementation of the authorization service at ASP.NET Core :
public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user,
object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
// Create a tracking context from the authorization inputs.
var authContext = _contextFactory.CreateContext(requirements, user, resource);
// By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
var handlers = await _handlers.GetHandlersAsync(authContext);
// Invoke all handlers.
foreach (var handler in handlers)
{
await handler.HandleAsync(authContext);
}
// Check the context, by default success is when all requirements have been met.
return _evaluator.Evaluate(authContext);
}
ASP.NET Core provides the IAuthorizationService
that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization.
public async Task CreateAsync(CreateModelDto input)
{
var result = await AuthorizationService
.AuthorizeAsync("Model_Management_Create_Books");
if (result.Succeeded == false)
{
//throw exception
throw new AbpAuthorizationException("...");
}
//continue to the normal flow...
}
Since this is a typical code block, ABP provides extension methods to simplify it.
public async Task CreateAsync(CreateModelDto input)
{
await AuthorizationService.CheckAsync("Model_Management_Create_Books");
//continue to the normal flow...
}
CheckAsync extension method throws AbpAuthorizationException
if the current user/client is not granted for the given permission. There is also IsGrantedAsync extension method that returns true or false.
Authorization handlers¶
An authorization handler is responsible for the evaluation of a requirement's properties. The authorization handler evaluates the requirements against a provided AuthorizationHandlerContext to determine if access is allowed.
A requirement can have multiple handlers. A handler may inherit AuthorizationHandlerTRequirement
is the requirement to be handled. Alternatively, a handler may implement IAuthorizationHandler directly to handle more than one type of requirement.
Use a handler for one requirement¶
public class PermissionRequirementHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly IPermissionChecker _permissionChecker;
public PermissionRequirementHandler(IPermissionChecker permissionChecker)
{
_permissionChecker = permissionChecker;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
if (await _permissionChecker.IsGrantedAsync(context.User, requirement.PermissionName))
{
context.Succeed(requirement);
}
}
}
User permission is checked with the permission check service provided by the ABP framework on which the PSS®X application is based.
When authorization is successful, context.Succeed
is invoked with the satisfied requirement as its sole parameter.
public class PermissionRequirement : IAuthorizationRequirement
{
public string PermissionName { get; }
public PermissionRequirement([NotNull] string permissionName)
{
Check.NotNull(permissionName, nameof(permissionName));
PermissionName = permissionName;
}
public override string ToString()
{
return $"PermissionRequirement: {PermissionName}";
}
}
Use a handler for multiple requirements¶
The following example shows a check multiple permission combinations.
public class PermissionsRequirementHandler : AuthorizationHandler<PermissionsRequirement>
{
private readonly IPermissionChecker _permissionChecker;
public PermissionsRequirementHandler(IPermissionChecker permissionChecker)
{
_permissionChecker = permissionChecker;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionsRequirement requirement)
{
var multiplePermissionGrantResult = await _permissionChecker.IsGrantedAsync(context.User, requirement.PermissionNames);
if (requirement.RequiresAll ?
multiplePermissionGrantResult.AllGranted :
multiplePermissionGrantResult.Result.Any(x => x.Value == PermissionGrantResult.Granted))
{
context.Succeed(requirement);
}
}
}
public class PermissionsRequirement : IAuthorizationRequirement
{
public string[] PermissionNames { get; }
public bool RequiresAll { get; }
public PermissionsRequirement([NotNull] string[] permissionNames, bool requiresAll)
{
Check.NotNull(permissionNames, nameof(permissionNames));
PermissionNames = permissionNames;
RequiresAll = requiresAll;
}
public override string ToString()
{
return $"PermissionsRequirement: {string.Join(", ", PermissionNames)}";
}
}
Handler registration¶
Register handlers in the services collection during configuration. Handlers can be registered using any of the built-in service lifetimes.
public class AbpAuthorizationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistered(AuthorizationInterceptorRegistrar.RegisterIfNeeded);
//...
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAuthorizationCore();
context.Services.AddSingleton<IAuthorizationHandler, PermissionRequirementHandler>();
context.Services.AddSingleton<IAuthorizationHandler, PermissionsRequirementHandler>();
//...
}
Purposed architecture of the PSS®X permissions system¶
A permission is a simple policy that is granted or prohibited for a particular user
, role
or client
.
Authorize attribute¶
ASP.NET Core defines the Authorize attribute that can be used for an action
, a controller
or a page
. Abp framework allows you to use the same attribute for an application service too.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
namespace GridLab.PSSX.Odms
{
[Authorize]
public class ModelAppService : ApplicationService, IModelAppService
{
public Task<List<ModelDto>> GetListAsync()
{
...
}
[AllowAnonymous]
public Task<ModelDto> GetAsync(Guid id)
{
...
}
[Authorize("Odms_Model_Create")]
public Task CreateAsync(CreateModelDto input)
{
...
}
}
}
-
Authorize attribute forces the user to login into the application in order to use the ModelAppService methods. So, GetListAsync method is only available to the authenticated users.
-
AllowAnonymous suppresses the authentication. So, GetAsync method is available to everyone including unauthorized users.
-
[Authorize("Odms_Model_Create")] defines a policy (see policy based authorization) that is checked to authorize the current user.
"Odms_Model_Create" is an arbitrary policy name. If you declare an attribute like that, ASP.NET Core authorization system expects a policy to be defined before.
Defining permissions¶
To define permissions, create a class inheriting from the PermissionDefinitionProvider
as shown below:
using Volo.Abp.Authorization.Permissions;
namespace GridLab.PSSX.Odms.Permissions
{
public class OdmsPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup("Odms");
myGroup.AddPermission("Odms_Model_Create");
}
}
}
Child Permissions¶
A permission may have child permissions. It is especially useful when you want to create a hierarchical permission tree where a permission may have additional sub permissions which are available only if the parent permission has been granted.
Example definition:
var modelManagement = myGroup.AddPermission("Model_Management");
modelManagement.AddChild("Model_Management_Create_Books");
modelManagement.AddChild("Model_Management_Edit_Books");
modelManagement.AddChild("Model_Management_Delete_Books");
For the example code, it is assumed that a role/user with "Model_Management" permission granted may have additional permissions. Then a typical application service that checks permissions can be defined as shown below:
[Authorize("Model_Management")]
public class ModelAppService : ApplicationService, IModelAppService
{
public Task<List<ModelDto>> GetListAsync()
{
...
}
public Task<ModelDto> GetAsync(Guid id)
{
...
}
[Authorize("Model_Management_Create")]
public Task CreateAsync(CreateModelDto input)
{
...
}
[Authorize("Model_Management_Edit")]
public Task UpdateAsync(CreateModelDto input)
{
...
}
[Authorize("Model_Management_Delete")]
public Task DeleteAsync(CreateModelDto input)
{
...
}
}
- GetListAsync and GetAsync will be available to users if they have Model_Management permission is granted.
- Other methods require additional permissions.
Permission Store¶
IPermissionStore is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system.
Add permissions claim to the user's identity¶
PSS®X application to store the user’s claims in a cookie which is read in with every HTTP request and turned into a ClaimsPrincipal
, which can be accessed in ASP.NET Core by the HttpContext property called “User”
-
UserPermissionValueProvider checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the AbpClaimTypes.UserId static property.
-
RolePermissionValueProvider checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the AbpClaimTypes.Role static property.
-
ClientPermissionValueProvider checks if the current client is granted for the given permission. This is especially useful on a machine to machine interaction where there is no current user. It gets the client id from the current claims. Client claim name is defined with the AbpClaimTypes.ClientId static property.
The second part is simpler and covers what happens every time the logged-in user accesses a protected action
, a controller
, a page
or application service
. Basically, we have a policy-based authorization with dynamic rules that checks the current User has the permission needed to execute the ASP.NET action/razor page.
Any access to a protected action causes a policy authorization check
We can extend the permission checking system by defining our own permission value provider
public class SystemAdminPermissionValueProvider : PermissionValueProvider
{
public SystemAdminPermissionValueProvider(IPermissionStore permissionStore)
: base(permissionStore)
{
}
public override string Name => "SystemAdmin";
public async override Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
{
if (context.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin")
{
return PermissionGrantResult.Granted;
}
return PermissionGrantResult.Undefined;
}
}
This provider allows for all permissions to a user with a User_Type
claim that has SystemAdmin value. It is common to use current claims and IPermissionStore in a permission value provider.
A permission value provider should return one of the following values from the CheckAsync method:
-
PermissionGrantResult.Granted is returned to grant the user for the permission. If any of the providers return Granted, the result will be Granted, if no other provider returns Prohibited.
-
PermissionGrantResult.Prohibited is returned to prohibit the user for the permission. If any of the providers return Prohibited, the result will always be Prohibited. Doesn't matter what other providers return.
-
PermissionGrantResult.Undefined is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission.
Once a provider is defined, it should be added to the AbpPermissionOptions as shown below:
Configure<AbpPermissionOptions>(options =>
{
options.ValueProviders.Add<SystemAdminPermissionValueProvider>();
});
Claims Principal Factory¶
Claims are important elements of authentication and authorization. PSS®X uses the IAbpClaimsPrincipalFactory service to create claims on authentication. If you need to add your custom claims to the authentication ticket, you can implement the IAbpClaimsPrincipalContributor in your application.
Add a OrganizationUnit claim and get it:
public class OrganizationUnitPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
{
public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
{
var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
var userId = identity?.FindUserId();
if (userId.HasValue)
{
var userService = context.ServiceProvider.GetRequiredService<IdentityUserManager>();
var user = await userService.FindByIdAsync(userId.ToString());
if (user != null)
{
user.OrganizationUnits
.Select(x => x.OrganizationUnitId).ToList()
.ForEach(unit => identity.AddClaim(new Claim(type: "organization_unit", value: unit.ToString())));
}
}
}
}
public class OrganizationUnitClaimPrincipalHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency
{
public Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
{
foreach (var claim in context.Principal.Claims)
{
if (claim.Type == "organization_unit")
{
claim.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken);
}
}
return Task.CompletedTask;
}
}
Register handler via OpenIddictClaimsPrincipalOptions;
Configure<AbpOpenIddictClaimsPrincipalOptions>(options =>
{
options.ClaimsPrincipalHandlers.Add<OrganizationUnitClaimPrincipalHandler>();
});
Get claim;
public static class CurrentUserExtensions
{
public static Guid[] GetOrganizationUnits(this ICurrentUser currentUser)
{
Claim[] claims = currentUser.FindClaims("organization_unit");
return claims.Select(c => Guid.Parse(c.Value)).ToArray();
}
}
State Checker¶
Any class can inherit IHasSimpleStateCheckers
to support state checks.
public class MyObject : IHasSimpleStateCheckers<MyObject>
{
public int Id { get; set; }
public List<ISimpleStateChecker<MyObject>> SimpleStateCheckers { get; }
public MyObject()
{
SimpleStateCheckers = new List<ISimpleStateChecker<MyObject>>();
}
}
The MyObject
class contains a collection of state checkers, you can add your custom checkers to it.
public class MyObjectStateChecker : ISimpleStateChecker<MyObject>
{
public Task<bool> IsEnabledAsync(SimpleStateCheckerContext<MyObject> context)
{
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
return Task.FromResult(currentUser.IsInRole("Admin"));
}
}
var myobj = new MyObject()
{
Id = 100
};
myobj.SimpleStateCheckers.Add(new MyObjectStateChecker());
Check the state¶
You can inject ISimpleStateCheckerManager<MyObject>
service to check state.
bool enabled = await stateCheckerManager.IsEnabledAsync(myobj);
Built-in State Checkers¶
The PermissionDefinition
, ApplicationMenuItem
and ToolbarItem
objects have implemented state checks and have built-in general state checkers, you can directly use their extension methods.
RequireAuthenticated();
RequirePermissions(bool requiresAll, params string[] permissions);
RequireFeatures(bool requiresAll, params string[] features);
RequireGlobalFeatures(bool requiresAll, params Type[] globalFeatures);