How to perform activity-based, data-aware authorization (ADA)

Introduction

As explained in the design guide, activity-based, data-aware authorization (ADA) is an access-control mechanism to authorize an authenticated identity access to the system based on permits granted on the operations (activities) and the associated data.

ASPSecurityKit aims to give you convention-based, first-class support for ADA in all the supported platforms – so you don’t need to write much authorization related logic yet get the full advantage of the same. Additionally, as flexibility and extensibility are the fundamental tenants of ASPSecurityKit design, you can always override and extend the conventions to suit your particular needs.

Note:

  • Read the design guide first as features mentioned in this guide are based on concepts explained over there.
  • In this guide, we’ll use the common term ‘operation’ to represent both the actions (in ASP.NET Mvc/Api/Core) and the request DTOs (ServiceStack).
  • For the sake of brevity, we’ll refer to ASP.NET on both .NET Framework and .NET Core as just ASP.NET. Both of these share a common ASPSecurityKit.net base library, which defines most of the attributes and other types mentioned in this guide.

ADA workflow

The IActivityPermittedHandler is invoked by the AuthorizationProvider to perform ADA. The default implementation (of ASP.NET or ServiceStack) works as follows:

  1. Determines a permissionCode for the requested operation based on the activity definition logic.
  2. If the requested operation has an auth definition, invoke it to determine if current identity is permitted. An auth definition is your custom logic that you write to override the default ADA flow.
  3. Otherwise, if the requested operation is marked with RequestFeature.PossessesPermissionCheckOnly, determine permit based on whether or not the current identity possesses a permit having the permissionCode obtained above (no data authorization).
  4. Otherwise, invoke the comprehensive data authorization process which basically discovers EntityIds mentioned in the request and authorizes each of such Ids.

In this article, we’ll first learn about the ADA infrastructure(first and fourth steps above). We’ll then introduce you to writing the auth definitions for operations that need complex custom authorization logic.

Activity definition

As learnt in ‘How to do activity-based authorization’, every action in ASP.NET (and every request dto in ServiceStack) is considered an activity. To authorize an activity, we need a corresponding PermissionCode. The security pipeline automatically builds up this code based on following conventions:

  • For ASP.NET, action and controller names are concatenated after removing the ‘Controller’ suffix. Reason behind this convention is simple: Often we create separate controller for each entity in our domain models and name that controller accordingly.
public Class AccountController : SiteControllerBase
{
    Public ActionResult Create()
    {
    }
}

For the Create action example above, the PermissionCode generated by convention will be ‘CreateAccount’.

  • For ServiceStack, request DTO name is assumed to be the complete PermissionCode:
[DataContract]
[Route("/accounts", "POST")]
public Class CreateAccount : IReturn<CreateAccountResponse>
{
}

For the CreateAccount dto example above, the PermissionCode generated by convention will be ‘CreateAccount’. * If your dto in general has a suffix – say request – which needs to be removed to generate the correct PermissionCode, you can set the DTOSuffix ASK’s ServiceStack setting. * You can also have similar convention for ServiceStack – having entity and action segments picked separately – by changing DTONameAsPermission setting to false. If you do that, the dto name is assumed to be the entity segment and HTTP method (request verb will be treated as the action segment.

Overriding the controller and action segments

You can individually override either or both the segments (entity or action) of a PermissionCode as follows:

  • For controller: Decorate it with the AuthEntityAttribute, specifying the code parameter with the value you desire for the entity segment:
[AuthEntity(“User”)]
public Class PermitController : Controller
{
    Public ActionResult Grant()
    {
    }
}

For the Grant action example above, the PermissionCode generated will be ‘GrantUser’.

  • For action: Decorate it with the AuthActionAttribute, specifying the code parameter with the value you desire for the action segment:
[AuthEntity(“User”)]
public Class PermitController : Controller
{
    [AuthAction("Edit")]
    Public ActionResult Grant()
    {
    }
}

In the example above we’ve used both AuthEntity and AuthAction attributes to explicitly specify entity and action that together make up a PermissionCode. Therefore, to authorize the Grant action, ASPSecurityKit will generate ‘EditUser’ as the PermissionCode.

Specifying the complete PermissionCode for an operation

AuthPermissionAttribute can be used to specify the complete PermissionCode in both ASP.NET and ServiceStack. It has the highest priority – so if it’s defined for an action, everything else will be ignored.

[AuthEntity(“User”)]
public Class PermitController : Controller
{
    [AuthAction("Edit")]
    [AuthPermission("NewPrivilege")]
    Public ActionResult Grant()
    {
    }
}

In the example above, the PermissionCode generated for Grant action will be ‘NewPrivilege’ because AuthPermission has overridden everything else.

Similarly, if AuthPermissionAttribute is defined for a controller, it’s assumed that you want to use the same PermissionCode for all the actions within that controller unless you decorate an action inside that controller with its own AuthPermissionAttribute.

In ServiceStack also, the AuthPermissionAttribute has the final word in determining the PermissionCode for a request dto.

Have your own custom conventions to determine PermissionCode

It’s possible that you may find the default conventions not suiting the particular needs of your application and you wish to have your convention to determine PermissionCode. Otherwise, you may end up using attributes excessively to specify PermissionCode which would be subject to manual errors in addition to a maintenance nightmare. Every ASPSecurityKit convention is designed keeping in mind that the customers should be able to extend/override it if need be. IRequestService.GetPermissionCode is invoked by authorizer to obtain the PermissionCode for the currently executing operation. Hence the logic discussed in previous sections is implemented within the platform specific implementation of this method such as NetCore, NetFramework Mvc, NetFramework WebApi or ServiceStack. You can inherit from the relevant platform’s RequestService implementation and override GetPermissionCode to change the convention for that platform. For more information, see extending RequestService.

Authorizing data

As learned in the data-aware authorization section of the design guide, activity authorization is not complete in and of itself until it can authorize users on the data being acted upon. Most operations performed in a web application involve some sort of existing data. In a multi-user/multi-tenant environment, data must be actively protected from being accessed and tampered with by users who don’t have a legitimate right to it.

In the same section we also learned about EntityId, which is basically a key that uniquely identifies a unit of data (record/document/object depending on the type of database being used). When an operation is executed with one or more EntityIds, we need to authorize at a minimum that the caller has sufficient privileges to perform that operation on those EntityIds. These privileges are grouped into a permit set which consists of a list of pairs of PermissionCodes and EntityIds.

Similar to how operations are identified and protected with activity checks, ASPSecurityKit is designed to protect data specified for each operation with equal rigor and flexibility, and with minimum assistance required from your part. At a high-level it works as follows:

  1. Traverse through the given input data recursively and capture such members that are deemed as EntityIds.
  2. Obtain related references for each such EntityId.
  3. Authorize related references of each such EntityId by checking whether the current identity holds a related permit. One permitted reference per EntityId is enough to authorize it.
  4. If any of the EntityIds couldn’t be authorized, forbid the execution of the operation and report to the caller about the unauthorized EntityId(s).

In the following sections, we’ll go through the in-depth details for the above steps.

preliminary checks

There are two checks performed before the above data authorization steps are attempted:

  1. If current identity possesses a general permit on the given PermissionCode, data authorization isn’t attempted and the call is considered authorized because a general permit is going to match any EntityId you give to it for the given PermissionCode. You should rarely use general permit for regular users – it serves best for admins with unrestricted rights on system data.
  2. If the current identity doesn’t possess any permit for the given PermissionCode, data authorization isn’t attempted and the call is considered unauthorized for obvious reasons – if there’s no such permission granted, the question of authorizing the data against that PermissionCode doesn’t arise.

Subsequent entity suspension check

Upon successful authorization of EntityIds, The entity suspension check is performed on the EntityIds and the failure (if any) is reported to the caller.

EntityId discovery

The EntityIdAuthorizer<TIdMemberReference> has Authorize methods one for each ASP.NET actions (that accepts a dictionary of action arguments) and ServiceStack request dtos (that accepts the dto object). The first step of authorization performed by these methods is to traverse through each of the given input members (object/parameters; including their nested properties in case of complex type) recursively and capture each such member that matches as an identifier (EntityId), based on the following rules:

  • The member has a non-nul value or has at least one element in case of a collection. This is the only rule that isn’t available for customization as it’s a preliminary traversal check. If you want to override it, you can but for that you need to write the traversal logic yourself.
  • The member name has ‘id’ or ‘username’ as a whole or as a suffix.
  • The member value is either a ValueType, a String or an array/generic collection/dictionary with element/value of type ValueType, a String.

The EntityId members are captured as instances of a type that implements IIdMemberReference which is specified at the time of wiring the EntityIdAuthorizer<TIdMemberReference> up. One implementation IdMemberReference<TIdReference> is already provided.

The traversal

The default traversal implementation can traverse and discover EntityIds from members of following types:

  • POCO classes – Only public properties are traversed.
  • Dictionary – Traversed if the type of the value is not of ValueType or String – the idea is that such type of dictionaries should rather be a collection of EntityIds as a whole rather than only some of its items. Both generic and non-generic dictionaries are supported.
  • IEnumerables – Traversed if the type of the items is not of ValueType or String – the idea is that such type of collections should rather be a collection of EntityIds as a whole rather than only some of its items. Both generic and non-generic IEnumerables are supported.

Aiding the EntityId discovery with attributes

It’s quite normal to have some members not conforming to the above conventions. ASPSecurityKit provides a couple of attributes to help you handle such situations without having to override the conventions.

  • You have an EntityId member – say by the name AccountNumber – and you need to authorize it. Decorate such property or action parameter with AuthorizeAttribute. The discovery logic considers use of AuthorizeAttribute as the final word in determining whether or not a given member is an EntityId.
  • You have a non-EntityId member – say username in ChangeUsername operation – and you don’t want it to be considered as EntityId as you can’t really authorize the new username being provided. Decorate such property or action parameter with DoNotAuthorizeAttribute. The discovery logic considers use of DoNotAuthorizeAttribute as the final word – if you end up putting it even on an object member, no further traversal will be done for even properties inside that object.

Overriding the EntityId determination logic

If the attributes mentioned above do not satisfy your needs – likely because you have particular naming or other conventions in the application – you should consider changing the default member matching rules. Use of attributes should be preferred for exceptional cases – and if something seems like a norm, it’s best to be served as a default convention. For example, if you have AccountNumber member as EntityId across several operations, rather than decorating all of them with AuthorizeAttribute, you should instead update the default member name matching rule. This way you can be sure that you do not miss authorizing AccountNumber value in some operation just because you forgot to put the AuthorizeAttribute on it.

You have multiple ways to customize the rules of matching a member as an EntityId:

  • To just customize the rule that matches the member name, change the default regex specified by the IdMemberSelectorRegex.
  • TO customize the overall predicate that executes the rules related to member’s name and its value type, change the default predicate specified by the IdMemberSelector. Important: since the default predicate registered also executes the rule for matching member name using the IdMemberSelectorRegex mentioned above, overriding it means that you’re responsible to match the name as well.

Regardless of the changes you make in the above selectors, the attributes described above continue to function the way described. The discovery logic doesn’t invoke IdMemberSelector for members that have one of those attributes defined.

Overriding the traversal

The traversal logic is defined in the protected method TraverseAndCapture which you can override using inheritance. You can also override the Authorize methods themselves if needed.

As we learned in the similar section of the design guide, related references are EntityIds at a higher level in relation to the EntityId being authorized and thus can be used to authorize it. After capturing EntityIds, EntityIdAuthorizer<TIdMemberReference> invokes IReferencesProvider<TIdMemberReference>.GetReferences to obtain related references. This step works as follows: For each EntityId,

  1. Check if a loader method is defined for the EntityId.
  2. If yes, get the EntityId collection to authorize along with their references for the given EntityId value. Why? Because the value of EntityId captured could be a collection (array) of EntityIds and we want to enable you to handle such members via loader the way you prefer. It’s also more efficient to load references in one query for multiple EntityIds if those are of the same entity type.
  3. Otherwise, attempt to register the EntityId as the only related reference because, eventual data authorization check only verifies permit against the references collection.

References loader methods

The ReferencesProviderBase implements IReferencesProvider<TIdMemberReference>. However, it’s not a complete implementation because the actual logic that loads references from the database isn’t defined. You’re expected to define this logic in a derived implementation of this class and hence it’s marked as abstract.

ReferencesProviderBase requires that you define a separate method – called a references loader – in your derived implementation for each EntityId that you expect to authorize using related references. It does the heavy lifting of registering and invoking the appropriate loader as per the given EntityId.

All source packages (including Essential) come with ready to use implementation of the derived ReferenceProvider with a few loader methods such as the following:

        public async Task<List<IdReference>> UserPermitId(Guid id)
        {
            return await dbContext.UserPermits
                .Where(x => x.Id == id)
                .Select(x => new List<Guid> { x.UserPermitGroup.UserId })
                .ToIdReferenceListAsync();
        }

The above loader method is defined for EntityId named UserPermitId and it returns related UserId as a reference which is then going to authorize UserPermitId specified in the request for the current operation.

There are two types of loader methods you can define in terms of type of the return value:

  1. Returns List<IdReference>: This is the most common type you’ll be using for most of your loader methods. It returns the related references for the given EntityId member as in the example above. The actual EntityId member remains the same – only its References property gets populated.
  2. Returns HashSet<IdMemberReference>: It’s called multi-EntityId loader. This one expects you to return a collection (HashSet) of EntityId members collection which is then returned to the authorizer and not the original EntityId member. You’re also expected to populate the References property of each member of the collection. Typically, this type of loader is used for an EntityId member having collection (array/list) of EntityIds as a value, each of which needs to be authorized separately.

As an example, consider you have an operation ‘RemovePermits’ which accepts an array of UserPermitIds to let callers remove multiple permits in one call. To authorize such collection of EntityIds, you can write a multi-EntityId loader as follows: Model definition:

public class RemovePermitModel
{
    [Required]
    public Guid[] UserPermitIds { get; set; }
}

Loader definition:

public async Task<HashSet<IdMemberReference>> UserPermitIds(Guid[] ids)
        {
            return new HashSet<IdMemberReference>((await dbContext.UserPermits
                .Where(x => ids.Contains(x.Id))
                .Select(x => new { x.Id, References = new List<IdReference> { x.UserPermitGroup.UserId } })
                .ToListAsync())
                .Select(x => new IdMemberReference("UserPermitId", x.Id, x.References)));
        }

In above loader example,

  1. UserPermitIds is the member name of the EntityId you have as an input member (a property) for the RemovePermits operation.
  2. When ReferencesProviderBase.GetReferences is invoked for the UserPermitIds member, the above loader will be called and it’s going to return a collection of IIdMemberReference each of which has a member name as UserPermitId (singular) but a different value.
  3. Each of the UserPermitId members is then going to be authorized using its References collection, which contains the UserId as the only item. If the authorization fails for any UserPermitId member, this operation call is considered unauthorized and the same is reported to the caller.

Notes:

  • You can define loader methods as either async or synchronous; you can have both implementations as well. GetReferencesAsync invokes the async loader if there’s one for the given EntityId; it invokes the synchronous one if there’s no async one. GetReferences Invokes the synchronous loaders and it doesn’t invoke async ones if there’s one because that leads to a deadlock. You should simply define only one version (either async or synchronous) depending on what you’ve chosen in your project.
  • If you want to tell ReferencesProviderBase not to consider a method as a loader method in your derived implementation, decorate it with NotAReferencesLoaderAttribute. Only needed if that method’s signature matches with any of the two loaders' syntax mentioned above.

Authorizing references and error reporting

After obtaining related references, we arrive at the final step of authorizing the EntityIds. The IsAuthorized method is invoked for this purpose.

For each EntityId in the provided collection, IsAuthorized checks if there’s a matching permit in the current identity’s permit set for any of its references and the given permissionCode. The PermissionCode can itself be modified for a reference because before every reference check, the call is made to GetPermissionCode. You should just return the provided PermissionCode if no change is required; that’s what the default implementation of IdReference<TId> does.

IsAuthorized considers current request as unauthorized in the following cases:

Upon successful authorization of EntityIds, entity suspension check is performed on these EntityIds. If that returns a failure, data authorization is considered as failed and the same is reported back to the caller.

Do it yourself with auth definitions

You can define an entirely custom authorization logic for one or more operations using the feature called authorize request definitions (auth definitions or AuthDefs for short). While doing so, you can still take advantage of the ADA infrastructure – including EntityId discovery, related references and entity suspension.

An auth definition is a method related to the operation it’s meant to authorize and is defined in your project. As mentioned in the ADA workflow section above, The IActivityPermittedHandler first checks to see if there’s an auth definition for the current operation. If so, it invokes the same and exits with the result the AuthDef returns.

Note: All source packages come with a base type – AuthDefinitionBase (as part of authorization) – that you can build your AuthDef containers upon (by inheriting from it). The Premium source packages additionally contains an AuthDef related to permit management.

Defining AuthDefs

To define AuthDefs, you need two things:

1. AuthDef method

As already mentioned, an AuthDef is simply a .NET method but with a special signature. In ASP.NET, it must be:

  • A public method.
  • Has a return type of AuthResult (or Task<AuthResult> for async definitions). Async AuthDefs aren’t only supported but encouraged.
  • Has the same name and accept similar parameters (type/order) as that of the action it is meant to authorize.
  • Additionally, has the last parameter as permissionCode (of type string).

In ServiceStack the signature must be:

  • A public method.
  • Has a return type of AuthResult (or Task<AuthResult> for async definitions). Async AuthDefs aren’t only supported but encouraged.
  • Has the name IsAuthorized (or IsAuthorizedAsync if you like to have async suffix).
  • Has exactly two parameters – request DTO and permissionCode (string).

2. AuthDef container

A container is simply a class that holds one or more AuthDefs. Use the same grouping logic you have to group actions in controllers (or methods in ServiceStack service classes).

In ASP.NET, a container class has a one-to-one relationship with the controller. This means that a container can only have AuthDefs related to actions defined in the same controller. This is because an ASP.NET action can have any number of parameters to capture request input and doesn’t bind you to use a single DTO request type. However, such flexibility can make it difficult to definitively identify which action corresponds to a given AuthDef if a match is attempted across all controllers.

A container is a regular DI component that can inject other dependencies.

Discovering and registering AuthDefs

The discovery and registration of AuthDefs happen during startup by the ASPSecurityKitRegistry component available separately in all supported platforms. The approach is the same in all ASP.NET (including .NET Core) related web frameworks (and hence the common ASPSecurityKitRegistryBase implements the logic) but a little different in ServiceStack.

ASP.NET

The Register method accepts four parameters related to auth definitions. These are authRequestDefinitionType, controllerType, registrationOptions and authRequestDefinitionRegistrar.

The authRequestDefinitionType parameter specifies the assembly in which AuthDef containers are located. authRequestDefinitionType parameter is mandatory if you want to kick off AuthDefs registration logic.

The controllerType parameter specifies the assembly in which controllers reside. It is only needed if your controllers and AuthDefs containers are placed in different assemblies.

The registrationOptions parameter specifies some flags to control the behavior related to action discovery as we see below.

The authRequestDefinitionRegistrar parameter specifies an instance of IAuthorizeRequestDefinitionRegistrar which manages auth definition registrations and defaults to an instance of AuthorizeRequestDefinitionRegistrar if not specified. In usual cases, you don’t need to override it.

If authRequestDefinitionType is specified, RegisterAuthRequestDefinitions is invoked which manages the discovery and registration process as follows:

  1. Discover a list of AuthDef containers.
    • Every type that’s a class, is not abstract, and has a name ending with suffix AuthDefinition is considered as an AuthDefs Container.
  2. For each AuthDefs container, perform the subsequent steps.
  3. Get a corresponding controller type from the controller assembly (if controllerType parameter was specified, look into its assembly; otherwise, look into the assembly of authRequestDefinitionType). An exception is thrown if no matching controller type is found or more than one is found.
    • If AuthDefs container type is decorated with AuthorizeControllerAttribute, consider the ControllerType as specified by it.
    • Otherwise, get the controller by the name as that of AuthDefs container type, with ‘AuthDefinition’ suffix changed to ‘Controller’.
  4. Register the AuthDefs container type with DI with request scope.
  5. Discover a list of AuthDef methods defined inside the container as per the rules specified in the AuthDef method section above.
  6. For each AuthDef method, find the matching action method in the controller type. If a matching action method isn’t found, the next step is skipped (AuthDef method registration doesn’t happen).
    • The name and parameter types (in the same order) are required for the match. You can additionally enforce parameters' names to match as well. If an action method is found by the same name but the parameters do not match, an exception is thrown (which you can change to do not throw).
    • Eventually if the action isn’t found, the default is to skip the authDefinition method (assuming that it could be a helper method). You can configure it to throw an exception instead.
  7. Finally, register the AuthDef along with the corresponding action method synchronously or asynchronously depending on the return type of the AuthDef method.

ServiceStack

The Register method accepts two parameters related to auth definitions. These are authRequestDefinitionType and authRequestDefinitionRegistrar.

The authRequestDefinitionType parameter specifies the assembly in which AuthDef containers are located. authRequestDefinitionType parameter is mandatory if you want to kick off AuthDefs registration logic.

The authRequestDefinitionRegistrar parameter specifies an instance of IAuthorizeRequestDefinitionRegistrar which manages auth definition registrations and defaults to an instance of AuthorizeRequestDefinitionRegistrar if not specified. In usual cases, you don’t need to override it.

If authRequestDefinitionType is specified, RegisterAuthRequestDefinitions is invoked which manages the discovery and registration process as follows:

  1. Discover a list of AuthDef containers.
  2. For each AuthDefs container, perform the subsequent steps.
  3. Register the AuthDefs container type with DI with request scope.
  4. Register static auth definitions. Static auth definitions only operate on the request DTOs and don’t need to be invoked in the context of AuthDef container instance (no AuthDef container DI resolution required).
  5. Discover a list of AuthDef methods defined inside the container as per the rules specified in the AuthDef method section above.
  6. Register the AuthDef synchronously or asynchronously depending on the return type of the AuthDef method.

Leveraging ADA

Since AuthDef containers are registered as any other DI service, you can inject components like IEntityIdAuthorizer, IReferencesProvider, ISuspensionService, IUserService and so on. Let’s give you some examples of how you can leverage these ASK components:

        public async Task<AuthResult> GrantAsync(UserPermit model, string permissionCode)
        {
            var result = await this.entityIdAuthorizer.IsAuthorizedAsync(permissionCode,
                new { UserPermitId = model.Id, model.UserId, model.UserPermitGroupId });

            if (result.IsSuccess)
            {
                return await this.entityIdAuthorizer.IsAuthorizedAsync(model.PermissionCode, new { model.EntityId });
            }

            return result;
        }