×
Back to book

Custom C#Bot ACL Security Filtering

This article demonstrates how to add custom server-side ACL security filtering to an application written with C#Bot.

Video

Scenario

This is a staff management app. Users have the ability to apply for leave by submitting a leave application . From the perspective of the user:

  • I want to be able to make any updates and have full CRUD control over my application
  • I also want to be able to nominate another user to be a reviewer for my application
  • A reviewer cannot update the application's Justification field.

Entity Model

The entity model consists of two entities:

  • An application entity with a Justification property and a workflow behaviour
  • A user entity with a user behaviour.
  • The application entity has both an applicant and reviewer reference to the user entity.

entityDiagram.png

Security Model

User has been given full CRUD permissions over the target application, as well as admin backend access. This is the first step of us implementing custom security, we will give full permissions in the model, and then limit permissions in special circumstances. Visitors have no permissions.

securityModel.png

Backend

UserApplicationEntity.cs

The class UserApplicationEntity is an ACL (Access Control List) that contains the security rules for CRUD access of Application entities by Users.

C#Bot will write out our security rules to reflect what we have specified in the security diagram. A typical security rule will look something like this:

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

The function name GetUpdate is called when the server-side is performing an update on an Application, and returns a boolean indicating if the user should be allowed to perform the action. In the case above, it is returning true, since we specified that Users can update Applications in the security model.

Each ACL will contain methods for create, update, read and delete that look like the example above, since they all implement the interface IAcl which describes what an ACL should contain.

Each of these CRUD ACL methods are given a copy of the current user, a list of the models/entities (in this case applications) and a SecurityContext which contains other services like the DbContext (which we can use to access the database).

The ACLs are checked after the entities/updates have been added the the DbContext change tracker, so the changes are queued, but have not yet been executed.

We can perform custom security logic in the Bot-Written ACLs to return true or false for particular cases we want to check. In our example, we want to make sure that a user who has created an application or is the applicant (through the applicant reference) of the application can make updates to their applications.

We can do that by turning the protected region on, and updating the GetUpdate method to look something like this:

public bool GetUpdate(User user, IEnumerable<IAbstractModel> models, SecurityContext context)
{
    // % protected region % [Override update rule contents here here] on begin
    if (context.DbContext.ChangeTracker.Entries().Any(x => x.Entity is ApplicationEntity))
    {
        var applicationEntityEntries = context.DbContext.ChangeTracker.Entries<ApplicationEntity>().ToList();

        if (applicationEntityEntries.All(x => x.Entity.Owner == user.Id || x.Entity.ApplicantId == user.Id))
        {
            return true;
        }
    }

    return false;
    // % protected region % [Override update rule contents here here] end
}

So now the new ACL update logic works like this:

  • Check that there are some ApplicationEntity entries in the DbContext change tracker.
    • Copy all the ApplicationEntity entries from the change tracker to a local variable.
    • Check that the user who is attempting to perform these changes on ApplicationEntity is either the Owner (created them) or is the Applicant (through the applicant reference).
      • If they are, return true and allow them to perform the updates.
  • Return false and prevent the updates from going through.

If I wanted to include the conditions to prevent a reviewer from updating certain fields on the application, I would use the following code.

if (applicationEntityEntries.All(x => x.Entity.ReviewerId == user.Id))
{
    var changedEntities = applicationEntityEntries.Select(x => x.Entity).ToList();

    var entityIds = changedEntities.Select(x => x.Id);

    var originalEntities = context.DbContext.ApplicationEntity
        .AsNoTracking()
        .Where(x => entityIds.Contains(x.Id))
        .ToList();

    var allJustificationsUnchanged = changedEntities
        .All(x => x.Justification == originalEntities.First(o => o.Id == x.Id).Justification);

    if (allJustificationsUnchanged)
    {
        return true;
    }
}

The logic is as follows:

  • Check that the user who is attempting the update is the nominated reviewer of the applications (by comparing the reviewer id on the application to the user id).
    • Pull out a copy of the ApplicationEntitys.
    • Get a list of IDs of all the ApplicationEntitys that are being changed.
    • Retrieve the original versions of there entities from the database.
    • Check that the Justification attributes have not been changed.
    • If Justification property has not been changed, allow the changes to go through.

With all the changes together, the code looks like this.

public bool GetUpdate(User user, IEnumerable<IAbstractModel> models, SecurityContext context)
{
    // % protected region % [Override update rule contents here here] off begin
    if(context.DbContext.ChangeTracker.Entries().Any(x => x.Entity is ApplicationEntity))
    {
        var applicationEntityEntries = context.DbContext.ChangeTracker.Entries<ApplicationEntity>().ToList();

        if (applicationEntityEntries.All(x => x.Entity.Owner == user.Id || x.Entity.ApplicantId == user.Id))
        {
            return true;
        }

        if (applicationEntityEntries.All(x => x.Entity.ReviewerId == user.Id))
        {
            var changedEntities = applicationEntityEntries.Select(x => x.Entity).ToList();

            var entityIds = changedEntities.Select(x => x.Id);

            var originalEntities = context.DbContext.ApplicationEntity
                .AsNoTracking()
                .Where(x => entityIds.Contains(x.Id))
                .ToList();

            var allJustificationsUnchanged = changedEntities
                .All(x => x.Justification == originalEntities.First(o => o.Id == x.Id).Justification);

            if (allJustificationsUnchanged)
            {
                return true;
            }
        }
    }
    return false;
    // % protected region % [Override update rule contents here here] end
}

After all our changes have been made, we can log in to the target application and test them out. The image below shows, through a network request, a review getting an error when trying to update an application they have changed the Justification property on.

failedUpdate.png