Start modelling your app today.

Get started for free

What's this?

C#Bot Custom Security

In this article we will customise the security of a C#Bot application to match some requirements that cannot be input to the security model.


Introduction and Modelling

For this article we are going to be referring to the following model.

Image

From this model we can see a department has many employees as well as many projects.

The requirement we are going to be fulfilling in this article is the following:

An employee can only view or edit projects owned by their department.

To start off with implementing this requirement we can set up a minimal security model on the platform.

The security diagram. This shows the employee entity with limited access.

How C#Bot Security Works

The security in C#Bot is entirely model based and this can be seen from the class structure inside the project. To view this in the code base, we can look in serverside/src/Models/ProjectEntity/ProjectEntity.cs.

Note the following block of code in this file:

[NotMapped]
public IEnumerable<IAcl> Acls => new List<IAcl>
{
    new EmployeeProjectEntity(),
    // % protected region % [Add any further ACL entries here] off begin
    // % protected region % [Add any further ACL entries here] end
};

This is a collection of ACL classes determining the security in place for the entity. We can take a look at the EmployeeDepartmentEntity ACL class to take a deeper dive into the workings of security. This file can be found at serverside/src/Security/Acl/EmployeeProjectEntity.cs.

For now we are going to take a look at the top of the class. The rest of the methods in the class follow a similar pattern.

public class EmployeeProjectEntity : IAcl
{
    public string Group => "Employee";
    public bool IsVisitorAcl => false;

    public bool GetCreate(User user, IEnumerable<IAbstractModel> models, SecurityContext context)
    {
        // % protected region % [Override create rule contents here here] off begin
        return false;
        // % protected region % [Override create rule contents here here] end
    }

    public Expression<Func<TModel, bool>> GetReadConditions<TModel>(User user, SecurityContext context)
        where TModel : IOwnerAbstractModel, new()
    {
        // % protected region % [Override read rule contents here here] off begin
        return model => true;
        // % protected region % [Override read rule contents here here] end
    }

To explain the key elements of the functionality of this class:

  • The Group field determines whether the ACL should be used to calculate the security rules for the user performing the operation. If the user is in the specified group then the ACL shall be evaluated.
  • The IsVisitorAcl field determines whether this security rule should apply to non logged in users to the system. It is important to note that any visitor ACLs shall also apply to any logged in users as well.
  • The GetCreate function evaluates whether the provided IEnumerable of models can be created. The security model is setup to prevent employees from creating projects, thus this function will return false in this scenario.
  • The GetReadConditions function determines a filter for data to be read from the database. This returns an expression is passed into an Entity Framework Where condition. This must return a filter since we do not know ahead of time what data is going to be fetched from the database.

There are other functions for the update and delete operations that follow the same patterns as well.

It is important to note the security service in the application will collect all relevant ACLs for the user performing the operation and combine them with a logical OR statement. This means the most permissive ACL will take precedence.

Customising ACLs

Now we have an understanding of how the ACLs work we can modify them to meet our requirement listed before. To recap

An employee can only view or edit projects owned by their department.

To do this we must edit the ACL class to check if the DepartmentId foreign key field on the employee entity matches the DepartmentId foreign key field on the Project entity.

Customising Read Security

We will start off on the implementation for the read operation. There are 2 different methods you can implement read security.

public Expression<Func<TModel, bool>> GetReadConditions<TModel>(User user, SecurityContext context)
    where TModel : IOwnerAbstractModel, new()
{
    // % protected region % [Override read rule contents here here] on begin
    if (typeof(TModel) == typeof(ProjectEntity) && user is EmployeeEntity employee)
    {
        Expression<Func<ProjectEntity, bool>> expression = model => model.DepartmentId == employee.DepartmentId;
        return (dynamic) expression;
    }
    return model => false;
    // % protected region % [Override read rule contents here here] end
}

You might notice that instead of declaring the expression inline in the return statement we separate it into a different statement and then return it casted to dynamic. This is since an ACL can be used on multiple different entities so there is no way for the compiler to know which ACL is used at compile time. Since the if statement in this method is using a runtime check to determine the validity of the operation, the type system doesn’t know about it. This means the only way to return the value is to cast it to dynamic and return the value. So long as the if statement exists, we can be assured the function will not incur any runtime errors from mismatched types.

Method 2

The only other option if we do not want to cast to dynamic is to build the expression at runtime. This is done by creating an Expression Tree.

public Expression<Func<TModel, bool>> GetReadConditions<TModel>(User user, SecurityContext context)
    where TModel : IOwnerAbstractModel, new()
{
    // % protected region % [Override read rule contents here here] on begin
    // Check if the provided types are not what we care about and return.
    if (typeof(TModel) != typeof(ProjectEntity) || !(user is EmployeeEntity employee))
    {
        return model => false;
    }

    // Create the expression parameter field and constant for the employee user
    // Due to the check above, the param will be of type ProjectEntity
    var param = Expression.Parameter(typeof(TModel));
    var userConstant = Expression.Constant(employee);

    // Extract the DepartmentId fields from the project parameter and the user
    var queryField = Expression.PropertyOrField(param, "DepartmentId");
    var userField = Expression.PropertyOrField(userConstant, "DepartmentId");

    // Return an expression that asserts that the both DepartmentId fields are equal
    return Expression.Lambda<Func<TModel, bool>>(
        Expression.Equal(queryField, userField),
        param);
    // % protected region % [Override read rule contents here here] end
}

While this code performs the same function as the method above, there is no casting to dynamic. It is therefore not recommended in most cases as it is more prone to runtime failures.

Customising Update Security

With the update there are 2 functions we must account for. These are the GetUpdateConditions and GetUpdate methods in the ACL.

The implementation for GetUpdateConditions is identical to what was done for the read conditions. It is recommended to abstract the logic of the read ACL into it’s own function so there does not need to be duplicated code for both read and update.

The implementation for GetUpdate is much simpler compared to the conditional statement since it does not require any runtime expression creation to validate the security. An example implementation of the problem would be as follows.

public bool GetUpdate(User user, IEnumerable<IAbstractModel> models, SecurityContext context)
{
    // % protected region % [Override update rule contents here here] on begin
    // Ensure the type of the user
    if (!(user is EmployeeEntity employee))
    {
        return false;
    }

    // Get the existing list of projects from the database.
    var projects = models
        .Where(m => m.GetType() == typeof(ProjectEntity))
        .Cast<ProjectEntity>()
        .ToList();
    var projectIds = projects.Select(p => p.Id).ToList();
    var existingProjects = context.DbContext.ProjectEntity
        .AsNoTracking()
        .Where(x => projectIds.Contains(x.Id))
        .ToList();

    // Loop over each model and if it is a project without a matching department id then fail
    // the security validation
    foreach (var project in projects)
    {
        var existingProject = existingProjects.FirstOrDefault(p => p.Id == project.Id);
        if (existingProject?.DepartmentId != employee.DepartmentId)
        {
            return false;
        }
    }
    return true;
    // % protected region % [Override update rule contents here here] end
}

This will ensure any update operations by employees will only succeed if they are in the same department as the project.

Last updated: 27 August 2020


Start modelling your app today.