Skip to content

Application Authorisation

01es edited this page May 15, 2020 · 22 revisions

Under review

The core principles for authorisation based on authorisation tokens and execution scoping, function as per the original specification. However, the Web UI side is different. Instead of the original intent, it now follows the same authorisation model as the domain model (i.e. authorisation tokens and execution scoping) in combination with menu visibility controls.

Our work on the model-driven Web API (GraphQL) requires further enhancement to the authorisation model in order to allow property-based read control (property-based write control is supported) and the control of the data fetch depth (i.e. how deep a query should reach into the sub-properties of a root entity type).

User Authorisation

An application user -- an entity that could represent a human or some other information system, which has been identified an authentic, and all associated with such user requests are considered to be authenticated. All requests to the systems (with rare exception) are required to be authenticated. Most authenticated requests require to be authorised.

Due to the fact that any authenticated request is always associated with a user, any request authorisation boils down to ensuring that the associated with that request user is authorised to perform the requested function of the system.

  1. General Principles
  2. Security Tokens
  3. Dynamic Authorisation Scopes
  4. UI Authorisation Scopes

General Principles

In explaining the general principles for TG authorisation model, a T32 security model (Security Matrix) should be used for comparison. In T32 there is a notion of security string -- a specific string value, which is associated with some UI control such as button, tab or potentially some event handler such as mouse double click. Since all UI controls are layered in a hierarchical structure (a button is on a panel, panel is on a tab, tab is no a form etc.) T32 builds a hierarchical representation of security strings called "Security Matrix".

There are several disadvantages with the approach used in T32, most significant of which are outlined below:

  • Glued to UI -- a change in a UI design requires modification security related changes; for example, a button was removed then security matrix needs to be adjusted by removing a relevant security string. An invocation of the same functionality from different places requires different security strings to be associated with different UI controls to be correctly represented in the security matrix, which could be confusing to the user setting up access permissions (and even error prone, by forgetting to restrict access to the same function in all places it gets invoked from). The business functionality, which is usually represented as methods/functions is not covered by T32 security model; the same business logic may be invoked directly by interacting with an UI control (this way it is under security) or as part of some other logic in which case it is not covered by the security check (authorisation process). Having a rich UI leads to a complex security matrix, with duplicate functionality under different UI controls and thus security strings potentially complicating the set up of user permissions.
  • Security strings -- a security string is just a string, which can have a typo and since it is glued to UI there is no easy way to automatically (unit test) ensure that it is used correctly in the application.
  • Imperative approach -- the current T32 security model is imperative, the developer has to explicitly implement a check for a security string by using a special library function and branching (e.g. if not(Allowed('Timesheet Review') in ['R','W']) then ...); this makes it more labor intensive to properly define authorisation scopes, and, most importantly, it restricts having dynamically nested scopes.

The developed in TG authorisation model revolves around the idea of an authorisation applied to methods of classes implementing entities and their companion objects, instead of UI controls. The developed model is declarative rather than imperative, and full customisation of the application specific authorisation mechanism (e.g. some customers may require LDAP driven user permission logic and some would be fine with the default DB driven approach).

TG security model consists of the following elements and principles:

  • Security token -- this is analogous to a security string, but instead of a string, TG utilises types (classes) implementing interface ISecurityToken; this way there can be no mistake of using an non-existing "security string" -- the compiler would complain in case the specified token does not exist.
  • Authorisation model -- a model providing the logic for checking user's permission to invoke methods that are in scope of a certain security tokens; this model is instantiated at the application start up time via IoC and may vary depending on specific requirements; for example, there can be a model supporting database driven authorisation such as in T32 where all permissions are stored in the database, or it could be some LDAP-based mechanism.
  • Declarative security -- the developer declares a need for a method call to be authorised by annotating that method with annotation Authorise, which takes a security token class as its default parameter; the system automatically takes care of performing the actual authorisation upon method invocation based on the specified security token and the application specific authorisation model; the same security token can be used for annotating multiple methods effectively putting them into the same security zone.
  • Authorisation result caching -- if user authorisation of any particular security token was evaluated at least once, the authorisation result can be cached for making all subsequent authorisations of the same token a very fast process; such caching should have an eviction strategy in order not to prevent dynamic modification of permissions (e.g. a time based eviction strategy could be a good default option).

Security Tokens

Unlike security strings, security tokens are fully fledged classes. One of the advantages of this is that compiler ensures the existence of security tokens used in the application. Another, is the ability to naturally form hierarchies. Being classes, security tokens can extend other tokens, thus composing a hierarchical structure of security tokens if necessary.

In order for a class to become a security token it needs to implement contract ISecurityToken. Security token classes never get instantiated as all the required information they carry is provided as part of their definition.

The following simple rules should be followed when implementing a new security token class:

  • All token classes should either implement ISecurityToken or be derived from a class implementing this interface. This information is used for establishing hierarchical relationships between security tokens for grouping purposes. Naturally, tokens directly implementing ISecurityToken are considered to be at the top of a hierarchical group of security tokens derived from them.
  • Annotation KeyTitle should be used to annotate each token class to provide meaningful descriptive information about the token.

Below is an example of the top level security token definition. The class name and the descriptive information speaks of the intent for this token to be used for authorising the deletion of entities if type FuelType. However, it still needs to be declared on a corresponding method that it should protect.

@KeyTitle(value = "Delete Fuel Type", desc = "Controls deletion of fuel types.")
public class DeleteFuelTypeToken implements ISecurityToken {
}

Here is a snippet of the FuelType companion object, which defines the authorisation scope of the above token:

...
@Authorise(DeleteFuelTypeToken.class)
public void delete(final TgFuelType entity) {
   defaultDelete(entity);
}
...

Dynamic Authorisation Scopes

A very important question is in relation to nested invocation of methods that have different authorisation tokens allocated to them with different permission level for the user making the invocation. In order to illustrate the practicality of this problem as well as to make it more obvious, let's review the following use case.

An application domain has four business entities -- WorkOrder, WorkOrderType, Vehicle and VehicleStatus. The application has authorisation rules that workshop personnel who create work orders should not be able to change information pertaining directly to vehicles. This is, for example, could be required to prevent workshop personnel from directly changing vehicle statuses, but they still might be able to view vehicle information, including vehicle status change history. In order to achieve this, there would need to be two security tokes -- WorkOrderSave, to authorise saving of work orders, and VehicleSave to authorise saving of vehicles. Under the above requirement, it seems only natural to deny workshop personnel from invoking methods that are annotated with @Authorise(VehicleSave.class), such as method VehicleDao.save(Vehicle):

...
@Authorise(VehicleSave.class)
public Vehicle save(final Vehicle veh) {...}
...

and allow those annotated with @Authorise(WorkOrderSave.class):

...
@Authorise(WorkOrderSave.class)
public WorkOrder save(final WorkOrder wo) {...}
...

Whenever workshop users tries to save vehicles their request get rejected due to authorisation restrictions, and work order saving should result in successful invocations. However, what happens when an allowed for invocation by a user method tries to invoke some other method that is not authorised for invocation by the same user?

To stay practical, let's consider the following use case. If a vehicle breakdown happens then a BR work order needs to be created. Upon saving of such a work order, the status of the associated vehicle should change to IR, indicating that the vehicle is in repair. This means that somewhere in method WorkOrderCo.save there would be a branching logic that would lead to the invocation of VehicleCo.save:

...
@Authorise(WorkOrderSave.class)
public WorkOrder save(final WorkOrder wo) {
    ...
    if (BR.equals(wo.getStatus()) {
       coVehicle.save(wo.getVehicle().setStatus(IR));
    }
    ...
}
...

So, what should happen? Should the restricted invocation to VehicleCo.save fail or succeed?

This is where dynamic authorisation scoping comes to play. The platform follows a very simple, yet very logical rule -- the top most authorisation scope wins. In the above case, there are two scopes. The top most is demarcated by @Authorise(WorkOrderSave.class) and the nested scope demarcated by @Authorise(VehicleSave.class). Due to the fact that workshop users are authorised to enter the top most scope, they're automatically allowed to enter any nested scope without any restrictions. This ensures correct execution of the method in question.

Clone this wiki locally