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
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.
Already downloading beta2, can wait until i test it. You are the best at dataobjects.net team!
ReplyDeleteGreat post, Dmitri, and great new sample application!
ReplyDeleteThanks Peter, thanks Alex.
ReplyDeleteP.S.
Peter, I think that every member in the team is of great importance. This is the only approach to build really great products.
This is beautiful! :) Good work guys!
ReplyDeleteI tested the sample app, I must say I love it a lot.
Time for me to MEF it up on MySQL.
Dmitri: I want to write "You guys are the best..." also with you, Alex, and others.
ReplyDeleteAnd also Malisa :-)
Thanks Malisa,
ReplyDeleteWill be waiting for the results of MEF + MySQL experiments.
I haven't followed the discussion on the new security system very close, but this looks promising :)
ReplyDeleteI'm currently planning a new project where we will most likely take use of it. We are building a system used internally by a company with several branch offices. Some users will need to access data belonging to several offices (not necessarily all of them).
As far as I can tell, I could solve this by using a "ManagerForSeveralBranchOfficesRole" and then have a "ManagerBranchOffices" collection on the user it applies to (or even as a separate entity). I would then use the filtering method inside the role to check that the branch office I'm working with is inside the "ManagerBranchOffices" role.
This might even be the best way of doing it, but I imagine that it would be nice to be able to put the branch office directly on the role, making it being a "Branch Office Mananger for [Branch Office Name]" role.
Do you have any comment on this? What is the recommended way of solving it?
Hello Terje,
ReplyDeleteThanks.
I'd make a role for manager of each branch, say SouthBranchOfficeManagerRole, NorthBranchOfficeManagerRole, etc. and put there the corresponding filtering for queries.
Having these roles defined it would be easy to combine them to grant access to all branches, to a subset or to only one of them.
Say, to grant access for a manager to SouthBranch & WestBranch you add SouthBranchOfficeManagerRole & WestBranchOfficeManagerRole to his collection of roles. DataObjects.Net is smart enough to union the results of these role-based query filters, so the manager will have access to the SouthBranch data and to the WestBranch data.
For more information about role-based query filtering see this post: http://blog.dataobjects.net/2011/06/on-security-system-part-6.html
Hi Dmitri
ReplyDeleteThat's fine as long as there are only a few (static) branch offices. It might be ok for us to do this now, since there are currently 5 offices and they will most likely not change.
I have a suggestion for an extension that maybe could be taken into consideration.
In a previous project we had several hundred units we needed to define roles on.
I ended up extending the built-in security system to have a role template "Branch Office Mananger" and each branch office got a "Branch Office Mananger for [City]" role that persons could get assign. The permissions were added to the role template, and the membership in that role was determined dynamically according to the current context. Meaning that the permissions given to the role template would be applied on objects having a context in one of your branch offices only.
I see that if I could be able to set properties on the roles and take use of these properties in the filter query, I'd have this flexibility built in. I don't know how you now persist the roles (by type name?) but you would have to add the possibility to store one or more entities together with it that could then be used in the permission query. I guess the loading of the roles is something that is done once and cached, so performance wise I currently don't see a big problem?
It would add quite a bit of flexibility and power, but I can understand if you think it gets too complex. At least the way I ended up building this around DO 3.8 got a bit messy :).
There is a different, though: In this version the permissions are defined in code and not through inheritance in the object structure. That removes a lot of the complexity we had - actually the only hard part was to define the permissions correctly - not assigning the roles.
What do you think? If you want to discuss this more or me to elaborate on it, please ask me.
Hi Dmitri
ReplyDeleteThat's fine as long as there are only a few (static) branch offices. It might be ok for us to do this now, since there are currently 5 offices and they will most likely not change.
I have a suggestion for an extension that maybe could be taken into consideration.
In a previous project we had several hundred units we needed to define roles on.
I ended up extending the built-in security system to have a role template "Branch Office Mananger" and each branch office got a "Branch Office Mananger for [City]" role that persons could get assign. The permissions were added to the role template, and the membership in that role was determined dynamically according to the current context. Meaning that the permissions given to the role template would be applied on objects having a context in one of your branch offices only.
I see that if I could be able to set properties on the roles and take use of these properties in the filter query, I'd have this flexibility built in. I don't know how you now persist the roles (by type name?) but you would have to add the possibility to store one or more entities together with it that could then be used in the permission query. I guess the loading of the roles is something that is done once and cached, so performance wise I currently don't see a big problem?
It would add quite a bit of flexibility and power, but I can understand if you think it gets too complex. At least the way I ended up building this around DO 3.8 got a bit messy :).
There is a different, though: In this version the permissions are defined in code and not through inheritance in the object structure. That removes a lot of the complexity we had - actually the only hard part was to define the permissions correctly - not assigning the roles.
What do you think? If you want to discuss this more or me to elaborate on it, please ask me.
Hi Dmitri
ReplyDeleteThat's fine as long as there are only a few (static) branch offices. It might be ok for us to do this now, since there are currently 5 offices and they will most likely not change.
I have a suggestion for an extension that maybe could be taken into consideration.
In a previous project we had several hundred units we needed to define roles on.
I ended up extending the built-in security system to have a role template "Branch Office Mananger" and each branch office got a "Branch Office Mananger for [City]" role that persons could get assign. The permissions were added to the role template, and the membership in that role was determined dynamically according to the current context. Meaning that the permissions given to the role template would be applied on objects having a context in one of your branch offices only.
I see that if I could be able to set properties on the roles and take use of these properties in the filter query, I'd have this flexibility built in. I don't know how you now persist the roles (by type name?) but you would have to add the possibility to store one or more entities together with it that could then be used in the permission query. I guess the loading of the roles is something that is done once and cached, so performance wise I currently don't see a big problem?
It would add quite a bit of flexibility and power, but I can understand if you think it gets too complex. At least the way I ended up building this around DO 3.8 got a bit messy :).
There is a different, though: In this version the permissions are defined in code and not through inheritance in the object structure. That removes a lot of the complexity we had - actually the only hard part was to define the permissions correctly - not assigning the roles.
What do you think? If you want to discuss this more or me to elaborate on it, please ask me.
Terje,
ReplyDeleteThanks for sharing the idea about role templates, it is outstanding! Moreover, it won't add any complexity.
As for the implementation, actually, this approach can be achieved within the current scheme. This is how:
1. A role has Name property. By default it returns the role type name, but this can be overridden. Role name is used as a role identifier and is persisted to the database.
2. There is no limitation on the number of role instances, the only thing is that a role must have unique name to distinguish one from another.
3. DataObjects.Net must know all roles that exist in an application. By default, it inspects all descendants of Role type and creates their instances (that's why parameterless onstructor is required), but this behavior is also can be overridden. So, you can create as much parameterized roles (or template-based roles in your terms) as you need and register them in DataObjects.Net.
4. That's it.
Now I guess I should make a separate post about this feature, what do you think?
Thanks, Dmitri. It sounds like a great idea :-)
ReplyDelete