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:
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 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 =
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
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.
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.