var blog = new ThoughtStream(me); RSS 2.0
 Sunday, July 27, 2008

Some coworkers were recently working on an object model for a simple security system. After some discussion with them, we came up with this basic model:

image

A permission is defined as an activity that can be assigned to a user, or group, and can be allowed or disallowed. From a Domain Driven Design perspective, we're stating that the Permission is the aggregate root. The User object itself, while involved in this aggregate, is divorced from this aggregate's relational model - you can load and work with a User object without having to load or worry about the Permission hierarchy. The addition of the "UserGroup" is solely for the many-to-many relationship between User and Group mappings with NHibernate and is not actually part of the object model's code.

Once we had this model in place, we wanted to have a simple query that allowed us to load a given permission object by activity name, for a user - whether the user was assigned directly or via a group. At a high level, this model and query should be fairly simple to work with. It turned out to be a massive learning curve in NHibernate, though.

After much trial, error, and Google searching, we ended up with this NHibernate query code:

ICriterion userIdMatches = Restrictions.Eq("Id", userId);
ICriterion activityNameMatches = Restrictions.Eq("Name", action);
ICriterion userIdAliasMatches = Restrictions.Eq("u.Id", userId);
 
DetachedCriteria groupPermissionCriteria = DetachedCriteria.For<Permission>()
    .SetProjection(Projections.Property("Group"))
    .CreateCriteria("Group").CreateCriteria("Users").Add(userIdMatches);
ICriterion groupSubquery = Subqueries.PropertyIn("Group", groupPermissionCriteria);
 
 
DetachedCriteria permissionCriteria = DetachedCriteria.For<Permission>()
    .CreateAlias("User", "u", JoinType.LeftOuterJoin)
    .Add(Restrictions.Or(userIdAliasMatches, groupSubquery));
permissionCriteria.CreateCriteria("Activity").Add(activityNameMatches);
 
 
ICriteria executableCriteria = permissionCriteria.GetExecutableCriteria(Session);
result = executableCriteria.List<Permission>();
 
 
return result;

There were a lot of lessons learned and a lot of parts that eventually got put together. Here's a quick run-down of what we ended up with and why.

  • The ICriterion's at the top of this code block are there to provide a little better readability in the real query code below.
  • The groupPermissionCriteria is set up to find a permission object where the specified userId belongs to a Group that belongs to the Permission - i.e. find permissions where the user is assigned via a group. The learning curve from this perspective was the SetProjection call. Though we are not entirely sure what a projection is at this point, we did find out that it was necessary for us to set this projection so that the detached criteria could be used as a sub-query.
  • The groupSubQuery is the conversion of the groupPermissionCriteria into an ICriterion so that we can do a logical Or with it in the primary query construction.
  • The permissionCriteria object sets up the core criteria logic and ties together the group permission assignment with the user permission assignment.
  • CreateAlias is used so that we can shorten the criteria for loading by the User assignment directly. The JoinType on the alias needs to be Left Outer Join so that we will return a proper Permission object even when there is no direct user assignment.
  • After creating the alias, we can add an "Or" criterion to the query and specify that we want to match based on the user's direct assignment or a group's assignment.
  • The last line of the criteria simply adds the Activity criteria to load by the activity name.

The resulting SQL will load the permission by Activity name AND (User assignment OR group assignment where the user is part of the group).

SELECT 
 
                this_.PERMISSIONSID as PERMISSI1_0_3_, 
                this_.IS_ALLOWED as IS2_0_3_, 
                this_.ACTIVITYID as ACTIVITYID0_3_, 
                this_.USERID as USERID0_3_, 
                this_.GROUPID as GROUPID0_3_, 
                activity1_.ACTIVITYID as ACTIVITYID4_0_, 
                activity1_.ACTIVITY_NAME as ACTIVITY2_4_0_, 
                activity1_.DESCRIPTION as DESCRIPT3_4_0_, 
                activity1_.INACTIVE_DATE as INACTIVE4_4_0_, 
                u2_.USERID as USERID3_1_, 
                u2_.USER_NAME as USER2_3_1_, 
                u2_.INACTIVE_DATE as INACTIVE3_3_1_, 
                group6_.GROUPID as GROUPID1_2_, 
                group6_.GROUP_NAME as GROUP2_1_2_, 
                group6_.INACTIVE_DATE as INACTIVE3_1_2_ 
FROM PERMISSIONS this_ 
                inner join ACTIVITY activity1_ 
                                on this_.ACTIVITYID=activity1_.ACTIVITYID 
                inner join USERS u2_ 
                                on this_.USERID=u2_.USERID 
                left outer join GROUPS group6_ 
                                on this_.GROUPID=group6_.GROUPID 
WHERE 
                activity1_.ACTIVITY_NAME = :p1
                and 
                (
                                u2_.USERID = :p2 
                                or 
                                this_.GROUPID = 
                                (
                                                SELECT 
                                                                this_0_.GROUPID as y0_ 
                                                FROM PERMISSIONS this_0_ 
                                                                inner join GROUPS group1_ 
                                                                                on this_0_.GROUPID=group1_.GROUPID 
                                                                inner join USERS_GROUPS users5_ 
                                                                                on group1_.GROUPID=users5_.GROUPID 
                                                                inner join USERS user2_ 
                                                                                on users5_.USERID=user2_.USERID 
                                                WHERE 
                                                                user2_.USERID = :p3
                                )
                )

Using a sub-query to load based on the group is not the most optimal way of loading the permission for the group assignment. However, since all of the joins in the main query and the sub-query are done on primary and foreign keys in the tables, performance should not be an issue. The only real performance concern for this query is the activity name in the where statement. A simple unique constraint and index on the activity name, though, will solve that problem.

Sunday, July 27, 2008 1:55:08 PM (Central Standard Time, UTC-06:00)  #    Comments [0]. Trackback 
Tags: .NET | Data Access | Domain Driven Design | NHibernate

Navigation
About Me
View Derick Bailey's profile on LinkedIn

Send mail to the author(s) Contact Me
Archive
<July 2008>
SunMonTueWedThuFriSat
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2008
Derick Bailey
Sign In
Statistics
Total Posts: 91
This Year: 91
This Month: 0
This Week: 0
Comments: 40
Themes
Pick a theme:
All Content © 2008, Derick Bailey
DasBlog theme 'Business' created by Christoph De Baene (delarou)