Monday 14 May 2012

Domain Based Security Part 2

Following on from my previous post, here is a more complex example of domain based security. Here are the business rules:
  • A user can edit their own tasks.
  • A basic user can edit another basic user's task, but not an admin user's task.
  • An admin user can edit an admin user's task or a basic user's task.
Again, the user entity will have a CanEditTask that takes a task as a parameter and returns a boolean.
There could be a list of permissions such as 'Edit tasks for basic user' or 'Edit tasks for admin user', but as the list of roles increases, this quickly becomes unmaintainable. It is more flexible to have a permission such as 'Edit tasks for users in role' which can be applied to a variety of roles. The way to do this is to break up the many-to-many relationship between the Role and Permission entities by inserting a new 'RolePermission' entity in the relationship. This entity links roles to the permission that they are entitled to and had references to the relevant Role and Permission entities, but has the benefit in that it can be inherited and introduce extra elements to the relationships.

For example a RolePermission object may hold a 'Basic user' role and a 'Edit own tasks' permission. This means that a basic user can edit their own tasks, but a RolePermissionForRole could inherit RolePermission, and this could have a 'Admin user' role, an 'Edit tasks for users in role' permission, and a RoleAppliesTo field of 'Basic user'. This means an admin user can edit tasks for a basic user. Here is how the UML class diagrams of the old and new systems compare.

First the previous system:


And the new one:


The user has a reference to it's own role, and consequently it's own permissions, and thus it knows which roles it is allowed to edit tasks of. The Task object has a reference to the User it is assigned to, and that user has has a reference it's role. So the CanEditTask method has access to all this information, and thus can work out whether the edit can take place. Here are the entites:
  
public class User : Entity
{
    protected string _username;
    protected Role _role;

    public virtual string Username
    {
        get { return _username; }
        set { _username = value; }
    }
               
    public virtual Role Role
    {
        get { return _role; }
        set { _role = value; }
    }

    public virtual bool CanEditTask(Task task)
    {
        List rolesWhosTasksCanBeEditied = _role
            .RolePermissions
            .Where(rolePermission => rolePermission.Permission.Key == PermissionKey.EditTaskForRole)
            .Cast()
            .Select(rolePermissionForRole => rolePermissionForRole.RoleAppliesTo)
            .ToList();

        if(rolesWhosTasksCanBeEditied.Contains(task.AssignedTo.Role))
            return true;
        else
            return false;
    }
}

public class Role : Entity
{
    protected string _rolename;
    private IList<RolePermission> _rolePermissions;

    public virtual string RoleName
    {
        get { return _rolename; }
        set { _rolename = value; }
    }

    public virtual IList<RolePermission> RolePermissions
    {
        get { return _rolePermissions; }
        set { _rolePermissions = value; }
    }
}

public class RolePermission : Entity
{
    protected Permission _permission;

    public virtual Permission Permission
    {
        get { return _permission; }
        set { _permission = value; }
    }
}

public class RolePermissionForRole : RolePermission
{
    private Role _roleAppliesTo;

    public virtual Role RoleAppliesTo
    {
        get { return _roleAppliesTo; }
        set { _roleAppliesTo = value; }
    }
}

public class Permission : Entity
{
    protected string _key;
    protected string _description;

    public virtual string Key
    {
        get { return _key; }
        set { _key = value; }
    }

    public virtual string Description
    {
        get { return _description; }
        set { _description = value; }
    }
}
public class Task : Entity
{
    private string _description;
    private User _assignedTo;

    public virtual string Description
    {
        get { return _description; }
        set { _description = value; }
    }

    public virtual User AssignedTo
    {
        get { return _assignedTo; }
        set { _assignedTo = value; }
    }
}
So now for the test to ensure this works as expected. A permission is created for editing a task for users in a specified role, and roles for basic user and admin user are created. Two RolePermissionForRole entities are created which give the permission to edit tasks, one for basic users' tasks, and one for admin users' tasks. Both of these RolePermissionForRole entities are added to the admin role, but only the RolePermissionForRole entity for editing a basic user's tasks is added to the basic user. Users and tasks are then created and it is asserted that a basic user can edit a basic user's task but not an admin user's task, and an admin user can edit both. Hope that makes sense! Here is the test to hopefully make it clearer:
  
[TestMethod]
public void CheckUserCanEditTasksBelongingToUsersInCertainRoles()
{
    Permission editTaskPermission = new Permission()
    {
        Key = PermissionKey.EditTaskForRole,
        Description = "Edit task for users in role."
    };

    Role basicRole = new Role()
    {
        RoleName = "Basic User",
        RolePermissions = new List<RolePermission>()
    };

    Role adminRole = new Role()
    {
        RoleName = "Admin User",
        RolePermissions = new List<RolePermission>()
    };

    RolePermissionForRole editTaskPermissionForBasicUser = new RolePermissionForRole()
    {
        Permission = editTaskPermission,
        RoleAppliesTo = basicRole
    };

    RolePermissionForRole editTaskPermissionForAdminUser = new RolePermissionForRole()
    {
        Permission = editTaskPermission,
        RoleAppliesTo = adminRole
    };

    basicRole.RolePermissions.Add(editTaskPermissionForBasicUser);
    adminRole.RolePermissions.Add(editTaskPermissionForBasicUser);
    adminRole.RolePermissions.Add(editTaskPermissionForAdminUser);

    User barryBasic = new User()
    {
        Username = "Barry Basic",
        Role = basicRole
    };

    User arnoldAdmin = new User()
    {
        Username = "Arnold Admin",
        Role = adminRole
    };

    Task arnoldsTask = new Task()
    {
        Description = "Arnold's Task",
        AssignedTo = arnoldAdmin
    };

    Task barrysTask = new Task()
    {
        Description = "Barry's Task",
        AssignedTo = barryBasic
    };

    Assert.IsFalse(barryBasic.CanEditTask(arnoldsTask));
    Assert.IsTrue(arnoldAdmin.CanEditTask(barrysTask));
}
And here is the NHibernate mapping files to map all this:
  
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="TaskLogger.Domain.Entities"
                   assembly="TaskLogger.Domain">
  <class name="User" table="`User`">
    <id name="Id" column="Id" type="long">
      <generator class="identity"/>
    </id>
    <property name="Username" type="string" />
    <many-to-one name="Role" class="Role" column="RoleId" cascade="save-update" />
  </class>
</hibernate-mapping>

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="TaskLogger.Domain.Entities"
                   assembly="TaskLogger.Domain">
  <class name="Role" table="`Role`">
    <id name="Id" column="Id" type="long">
      <generator class="identity"/>
    </id>
    <property name="RoleName" type="string" />
    <bag name="RolePermissions"  >
      <key column="RoleId"/>
      <one-to-many class="RolePermission"/>
    </bag>
  </class>
</hibernate-mapping>

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="TaskLogger.Domain.Entities"
                   assembly="TaskLogger.Domain">
  <class name="RolePermission" table="`RolePermission`">
    <id name="Id" column="Id" type="long">
      <generator class="identity"/>
    </id>
    <many-to-one name="Permission" class="Permission" column="PermissionId" cascade="save-update" />

    <joined-subclass name="RolePermissionForRole" table="`RolePermissionForRole`">
      <key column="`Id`"/>
      <many-to-one name="RoleAppliesTo" class="Role" column="RoleAppliesToId" cascade="save-update" />
    </joined-subclass>
    
  </class>
</hibernate-mapping>

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="TaskLogger.Domain.Entities"
                   assembly="TaskLogger.Domain">
  <class name="Permission" table="`Permission`">
    
    <id name="Id" column="Id" type="long">
      <generator class="identity"/>
    </id>
    
    <property name="Key" column="`Key`" type="string"/>
    <property name="Description" type="string" />
    
  </class>
</hibernate-mapping>

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="TaskLogger.Domain.Entities"
                   assembly="TaskLogger.Domain">
  <class name="Task" table="`Task`">
    <id name="Id" column="Id" type="long">
      <generator class="identity"/>
    </id>
    <many-to-one name="AssignedTo" class="User" column="AssignedToId" cascade="all" />
    <property name="Description" type="string" />
  </class>
</hibernate-mapping>

No comments:

Post a Comment