Today the DataObjects.Net Team is releasing the second beta of the upcoming 4.5 version of DataObjects.Net. The beta is mostly dedicated to the security system.
All security-related classes are located in a separate assembly called Xtensive.Practices.Security. Let's explore what types to be aware of the assembly includes:
- IPrincipal, Principal & GenericPrincipal
- Role & RoleSet
- Permission & PermissionSet
- IHashingService with several implementations
- IPrincipalValidationService with a generic implementation
- ImpersonationContext
- some Session extension methods
Most of them have been already discussed in the previous posts on the topic. However, let's recall the main principals & usage scenarios on a security sample called SalesPoint.
Adding the security system to your project
1. Add reference to Xtensive.Practices.Security assembly
2. Register types from Xtensive.Practices.Security in your domain.
<domain connectionurl="sqlserver://./SalesPoint"upgrademode="Skip">
<types>
<add assembly="SalesPoint">
<add assembly="Xtensive.Practices.Security">
</types>
</domain>
Xtensive.Practices.Security contains the following persistent types: Principal, GenericPrincipal (both are abstract) and PrincipalRole which is used for storing role names for a principal.
3. Make your own persistent type that describe a user of your application. Inherit it from Principal or GenericPrincipal classes.
Principal class defines the minimum IPrincipal implementation and can be used for all kind of users, no matter whether you use login/password authorization scheme, Windows-based one or your own.
GenericPrincipal class is specifically designed for login/password authorization scheme. It already contains all the required properties and methods for storing password hash, for setting password, etc. Moreover, generic principal validation service is oriented only on GenericPrincipal class and its inheritors. So, in the overwhelming majority of scenarios you should inherit from GenericPrincipal class.
In the sample the login/password authorization scheme is used, so the Employee class is inherited from the GenericPrincipal one. Note that you have total control on properties of user hierarchy: you define the hierarchy inheritance scheme as well as the structure of Key fields.
[HierarchyRoot]
public class Employee : GenericPrincipal
{
[Field, Key]
public int Id { get; private set; }
// Other fields
}
More about principal types:
part 5.
4. Define which hashing algorithm will be used. In this version md5, sha1, sha256, sha384, sha512 hashing algorithms are provided plus plain one that allows to store passwords as is, without hashing. This might be useful for testing purposes but it is strongly recommended to use true hashing one in the real life applications. In the sample the plain one is used, here is how it can be configured:
<configSections>
<section name="Xtensive.Security" type="Xtensive.Practices.Security.Configuration.ConfigurationSection, Xtensive.Practices.Security" />
</configSections>
<Xtensive.Security>
<hashingService name="plain"/>
</Xtensive.Security>
If no hashing service is set, then the system will fall back to the 'plain' one.
Managing passwords and validating users
Having the above-mentioned steps done, you are getting the ability to manage users, set their passwords and validate them. Here is how:
using (var session = domain.OpenSession()) {
using (var trx = session.OpenTransaction()) {
var employee = new Employee(session);
employee.Name = "Steve Ballmer";
employee.SetPassword("developers, developers, developers, developers");
trx.Complete();
}
}
using (var session = domain.OpenSession()) {
using (var trx = session.OpenTransaction()) {
var employee = session.ValidatePrincipal("Steve Ballmer", "developers, developers, developers, developers");
Assert.IsNotNull(employee);
trx.Complete();
}
}
Defining roles
Whereas user management is the essential part of any security system, roles sometimes are used as an optional addition, thus loosing all their power. But it's you who decide whether to employ them or not. In Xtensive.Practices.Security roles are optional too.
Roles are ordinary classes, not persistent ones, and so are permissions. In order to utilize the roles mechanism you inherit from base Role<T> class and declare permissions for domain model types, like here:
public class EmployeeRole : Role
{
public EmployeeRole()
{
// This is base role for every employee.
// It defines read-only access to products and employees for all staff.
// All employees can see products
RegisterPermission(new Permission<Product>());
// All employees can see employees
RegisterPermission(new Permission<Employee>());
}
}
public class StockManagerRole : EmployeeRole
{
public StockManagerRole()
{
// Stock manager inherits all permissions from Employee role
// In addition, it declares write access to products
// Stock managers can see and edit products
RegisterPermission(new Permission<Product>(canWrite:true));
}
}
Here is more advanced role declaration. You can declare secure queries for persistent types and use you own permission classes as well:
public class SalesRepresentativeRole : EmployeeRole
{
private static IQueryable<Order> GetOrdersQuery(ImpersonationContext context, QueryEndpoint query)
{
// Sales representative role has access to its own orders only
return query.All<Order>()
.Where(o => o.Employee == context.Principal);
}
public SalesRepresentativeRole()
{
// Sales representative inherits Employee permissions
// Sales representative can see and edit customers
RegisterPermission(new Permission<Customer>(canWrite:true));
// Sales representative can see and edit sale orders but not approve
RegisterPermission(new OrderPermission(canWrite:true, canApprove:false, GetOrdersQuery));
}
}
Important: for the ease of use give your roles parameterless constructors. If this is unacceptable, then you should provide to a framework a list of role instances. I'll describe this this later.
More about roles and permissions:
part 2,
part 3,
part 4.
Impersonation
The next question after we've successfully defined users, authentication scheme, roles and permissions is "How the hell will these parts work together?". Let's see.

You already know how to validate user by the pair of name and password.
If the validation is successful, you may impersonate current Session with user's account. After this is done, the security framework will provide you with all necessary infrastructure on what permission the user has, which roles he is in.
using (var session = domain.OpenSession()) {
using (var trx = session.OpenTransaction()) {
var employee = session.ValidatePrincipal("Steve Ballmer", "developers, developers, developers, developers");
// Opening an impersonation context
var context = session.Impersonate(employee);
// Checking permissions
context.Permissions.Contains<Permission<Customer>>(p => p.CanRead);
context.Permissions.Contains<OrderPermission>(p => p.CanApprove);
// Closing the context
context.Undo();
trx.Complete();
}
}
Here is how permissions are used to restrict screens of the sample application:
Note that currently logged in employee is in StockManager role that prohibits access to Customers and Orders. The main menu just checks whether current impersonation context contains the appropriate permission and acts accordingly by enabling or disabling controls.
Probably, the most exciting and important feature of the framework is automatic application of secure filters to every query that is being executed through Session.Query endpoint.
For example, if we log in with an employee who is in SalesRepresentative role, which restricts all orders to his own orders only, we will see the following picture:
We logged in as a Robert King, a sales representative. He can see only his own orders as the filter is declared in SalesRepresentative role and is applied automatically. The application doesn't know about users, permissions, roles, etc. All this stuff is provided by Xtensive.Practices.Security layer. Note that Customers and Orders views are available to him, but the "Approve" button is not, because the role doesn't declare access to "Approve" action.
What will happen if we log in as SalesManager? Let's see:
The user also has access to Customers and Orders as SalesManager role inherits it from SalesRepresentative one. Moreover, the role doesn't have such restriction for Orders, the employee see not only his own orders but the orders of employees from his department. In addition, the button "Approve" is enabled.
More on applying security filters to queries:
part 6
A few notes about the impersonation context
ImpersonationContext class implements IDisposable, so you can employ
using pattern. The context also supports nesting.
Current impersonation context instance can be accessed through Session, so you don't have to pass the reference to the context everywhere you might need it.
var context = session.GetImpersonationContext();
How to build the SalesPoint sample application
SQL script that builds the required "SalesPoint" database is included into the solution "Samples" and is located in the root folder of the SalesPoint project. Run the script against your database server and check that the database is successfully created. After that update the connectionUrl in app.config file according to the
path of your database server.
Download
DataObjects.Net 4.5 Beta 2 can be downloaded from the
website.
P.S.
Dear users of DataObjects.Net!
This is DataObjects.Net Beta 2, so I'm asking for your feedback. Play with the sample or try creating your own one, try applying the security framework to your real projects, do anything you want with it including the most weird scenarios (I know you can), I'll be happy to receive any comment no matter good or bad from you. Any of these will definitely help to make the product better and keep our requirements for high quality product.
Thank you.