This is the forth part in a series of posts that is dedicated to security system concept in DataObjects.Net. In the
previous part I described Role & Permission types and their relationships in context of entity access based on entity type. This part of the concept answers the question whether a user has access to entities of a persistent type or not and what kind of access if yes (read, write, approve, you name it).
In the part where the requirements were listed there was another set that defines some business rules applied to the set of entities that are accessible to a user. Remember, Sales Representatives can edit only their own Order instances, while Sales Managers can edit both their own orders as well as orders of stuff from their Sales Department and so on.
Let me recall the matrix:
So, we should have an option to restrict the set of Customers and Orders visible for Sales Representative & Sales Manager roles. These requirements are more like full-fledged business rules applied to the entire set of Customers & Orders than just a security issue, thus they rarely can be implemented in terms of ACL model.
Having Permission<T> class we can add there a property that will hold an IQueryable<T> object that will be used to describe the entire set of entities of type T that is accessible to a role that contains the permission.
Here is the definition of the Permission<T> class:
public class Permission<T> : Permission where T : class, IEntity
{
public Func<SecurityContext, IQueryable<T>> Query { get; protected set; }
...
public Permission(bool canWrite)
: base(typeof(T), canWrite)
{
// If not set explicitly, all entities are accessible by default
Query = context => context.Session.Query.All<T>();
}
public Permission(bool canWrite, Func<SecurityContext, IQueryable<T>> query)
: base(typeof(T), canWrite)
{
Query = query;
}
}
Having such an option, we can use it to explicitly define the accessibility-related business rules. Let's start with Sale Representative. This role restricts access to Order entities to those that are created by particular sales representative. So we have to define a rule that will build an IQueryable<Order> and register it properly. Here is how:
public class SalesRepresentativeRole : EmployeeRole
{
private static IQueryable<Order> GetOrdersQuery(SecurityContext context)
{
// Sales representative role has access to its own orders only
return context.Session.Query.All<Order>()
.Where(o => o.Employee == context.User);
}
public SalesRepresentativeRole()
{
RegisterPermission(new OrderPermission(canWrite:true, canApprove:false, GetOrdersQuery));
}
}
I won't focus on SecurityContext for now, The only thing that we should know about it is that it contains a Session and a User.
Note that we use Func<SecurityContext, IQueryable<T>> because we need to dynamically resolve the IQueryable<T> depending on the security context, currently active session, etc.
Using the same technique we can define restrictions for Sales Manager role:
public class SalesManagerRole : SalesRepresentativeRole
{
private static IQueryable<Order> GetOrdersQuery(SecurityContext context)
{
// Sales manager role has access to its own orders as well as to orders of his department
return context.Session.Query.All<Order>()
.Where(
o =>
o.Employee == context.User ||
o.Employee.In(
context.Session.Query.All<Employee>().Where(e => e.ReportsTo == context.User)));
}
public SalesManagerRole()
{
// Sales manager can do sale orders approval, in addition
RegisterPermission(new OrderPermission(canWrite:true, canApprove:true, GetOrdersQuery));
}
}
Note that actually we don't need ACLs or something artificial for defining the restrictions. Imagine a database with thousands and billions of entities and every single one must have one or more records that store entity's owner & all users that should have access to it. That's not an option. Pure business objects from the domain model and their relationships are enough for business rules of almost arbitrary complexity.
Now let's define business rules for Sales Representative & Sales Manager roles for accessing Customer objects. There is a rule that says that both of them should deal only with those customers that are from their local region. In this sample there are 2 sales departments, one is located in London, another is in Seattle. Thus, I split all customers into 2 groups: customers from the Old World & customers from the New World. The first group is served by the London sales department, the second one is by the Seattle one.
To simplify things I added a helper class that contains 2 groups of countries:
public static class WellKnown
{
public static IList<string> NewWorldCountries;
public static IList<string> OldWorldCountries;
static WellKnown()
{
NewWorldCountries = new List<string>()
{
"Argentina", "Australia", "Brazil", "Canada",
"Mexico", "USA", "Venezuela"
};
OldWorldCountries = new List<string>()
{
"Austria", "Belgium", "Denmark", "Finland",
"France", "Germany", "Ireland", "Italy",
"Japan", "Netherlands", "Norway", "Poland",
"Portugal", "Singapore", "Spain", "Sweden",
"Switzerland", "UK"
};
}
}
Having that done, I can describe the customers-related business rule as follow:
public class SalesRepresentativeRole : EmployeeRole
{
private static IQueryable<Customer> GetCustomersQuery(SecurityContext context)
{
// Sales representative role has access to local customers only
var employee = (Employee)context.User;
if (employee.Address.Country.In(WellKnown.NewWorldCountries))
return context.Session.Query.All<Customer>()
.Where(c => c.Address.Country.In(WellKnown.NewWorldCountries));
else
return context.Session.Query.All<Customer>()
.Where(c => c.Address.Country.In(WellKnown.OldWorldCountries));
}
public SalesRepresentativeRole()
{
// Sales representative can see and edit customers
RegisterPermission(new Permission<Customer>(canWrite:true, GetCustomersQuery));
}
}
Is't that good? As the role structure is hierarchical, this business rule is automatically inherited by all descendants of Sale Representative role, if not overridden. This means that there is no need to duplicate it in Sales Manager role. However, as Sales President role shouldn't have such a restriction we properly override that permission:
public class SalesPresidentRole : SalesManagerRole
{
public SalesPresidentRole()
{
// Overriding the inherited permission with restriction, so this role will have access to all customers
RegisterPermission(new Permission<Customer>());
...
}
}
That's all for today. In the next part I'll describe the SecurityContext class and related stuff in details.
Big note for all:
The security concept I'm talking about is a prototype. Its purpose is to find out the better way to implement the security in DataObjects.Net. The only way I can see this can be done right is to criticize & discuss the thing before it is implemented and released. I know that the most of DataObjects.Net users are mature and experienced developers that have numerous properly built applications that have their own security systems. Therefore, I'm hoping that it will be better to join our efforts and make the security system that will perfectly fit our needs.
For now, I would like to specially thank Vlad Klekovkin for his critics of the prototype. Vlad also shared a concept of his own security system built on top of DataObjects.Net.
Thanks a lot, Vlad!