Rhino Security: Part II - Discussing the Implementation
I just finished testing an annoying but important feature, NHibernate's second level cache integration with Rhino Security. The security rules are a natural candidate for caching, since they change to change on an infrequent basis but are queried often. As such, it is obvious why I spent time ensuring that the whole thing works successfully.
At any rate, what I wanted to talk about today was structure of the framework. You can see the table layout below.
A few things to note:
- The tables are all in the "security" schema, I find that it makes more sense this way, but you can set it to work with "security_Operation" if you really like (or the DB doesn't support schemas).
- User is referenced a few times, but is not shown here. As I mentioned earlier, the user entity is an external concept. We are using the mapping rewriting technique that we discussed earlier.
(more below)
Here is the main interface for Rhino Security:
The tasks it performs are:
- Enhance a query with security information.
- Get an explanation about why permission was given / denied.
- Perform an explicit permission check on entity or feature.
The last two are fairly easy to understand, but what does the last one means? It means that if I want to perform a "select * from accounts" and I enhance the query, I will give this baby:
SELECT THIS_.ID AS ID4_0_,
THIS_.SECURITYKEY AS SECURITY2_4_0_,
THIS_.NAME AS NAME4_0_
FROM ACCOUNTS THIS_
WHERE @p0 = (SELECT THIS_0_.ALLOW AS Y0_
FROM SECURITY_PERMISSIONS THIS_0_
INNER JOIN SECURITY_OPERATIONS OP1_
ON THIS_0_.OPERATION = OP1_.ID
LEFT OUTER JOIN SECURITY_ENTITIESGROUPS ENTITYGROU2_
ON THIS_0_.ENTITIESGROUP = ENTITYGROU2_.ID
LEFT OUTER JOIN SECURITY_ENTITYREFERENCESTOENTITIESGROUPS ENTITIES7_
ON ENTITYGROU2_.ID = ENTITIES7_.GROUPID
LEFT OUTER JOIN SECURITY_ENTITYREFERENCES ENTITYKEY3_
ON ENTITIES7_.ENTITYREFERENCEID = ENTITYKEY3_.ID
WHERE OP1_.NAME IN (@p1,@p2)
AND (THIS_0_."User" = @p3
OR THIS_0_.USERSGROUP IN (@p4))
AND ((THIS_.SECURITYKEY = THIS_0_.ENTITYSECURITYKEY
OR THIS_.SECURITYKEY = ENTITYKEY3_.ENTITYSECURITYKEY)
OR (THIS_0_.ENTITYSECURITYKEY IS NULL
AND THIS_0_.ENTITIESGROUP IS NULL))
ORDER BY THIS_0_.LEVEL DESC,
THIS_0_.ALLOW ASC);
Isn't it cute? This is much better than some home grown security system that I have seen (some of which I have built, actually), since it is stable with regards to the cost of the query that it will use. I mention before that it is possible to de-normalize the information into a single table, but since this requires more invasive approach from the application side, and since I have not seen performance issues with this yet, I'll leave it at this point for the moment.
Advance: Working with non DB backed users
In most applications, the user is a fully fledged entity, even if your authentication is elsewhere. Just keeping things like preferences will necessitate that, but let us assume that we really have that as a non DB entity.
In this case, we would need to make modifications to the tables (which we generally do, anyway, although automatically), and re-map the user references as an IUserType that can get the user from an id instead of a foreign key to entity. I think I'll wait until someone really needs it to make a demo of it.
Anyway, this is the main magic. It was fairly hard to get it right, as a matter of fact. But the query enhancement was one of the things that made security a breeze in my last project. We had a custom security module, but many of the concepts are the same.
The next part we will talk a bit about the IsAllowed and how that works. There is some nice magic there as well.