So, Bob is the manager at Company A. He gets the admin role at all levels. James is the manager for Company A/North. He gets the admin role for Company A/North, but he is allowed to run reports on Company A's other regions. Tim is an engineer for RecursiveTown - and he just has the engineer role for that town.
Let's have some (very) basic table structures that meet our needs, data-wise:
CREATETABLE[dbo].[Users] ( ID INTIDENTITY (1, 1) NOTNULLPRIMARYKEYCLUSTERED, [Name]NVARCHAR (50));CREATETABLE[dbo].[Roles] ( ID INTIDENTITY (1, 1) NOTNULLPRIMARYKEYCLUSTERED, [Name]NVARCHAR (50));CREATETABLE[dbo].[Containers] ( ID INTIDENTITY (1, 1) NOTNULLPRIMARYKEYCLUSTERED, ParentContainerID INTFOREIGNKEYREFERENCES[dbo].[Containers] (ID) ONDELETENOACTIONONUPDATENOACTION, [Name]NVARCHAR (50));CREATETABLE[dbo].[UserContainerRoles] ( ID INTIDENTITY (1, 1) PRIMARYKEYCLUSTERED, ContainerID INTNOTNULLFOREIGNKEYREFERENCES[dbo].[Containers] ([ID]) ONDELETENOACTIONONUPDATENOACTION, RoleID INTNOTNULLFOREIGNKEYREFERENCES[dbo].[Roles] ([ID]) ONDELETENOACTIONONUPDATENOACTION, UserID INTNOTNULLFOREIGNKEYREFERENCES[dbo].[Users] ([ID]) ONDELETENOACTIONONUPDATENOACTION);
Simple... Let's look at the data that might be within them:
Users
ID | Name |
---|---|
1 | Bob |
2 | James |
3 | Tim |
Roles
ID | Name |
---|---|
1 | Administrator |
2 | Reporter |
3 | Engineer |
Containers
ID | ParentContainerID | Name |
---|---|---|
1 | <NULL> | Company A |
2 | 1 | North |
3 | 1 | South |
4 | 1 | East |
5 | 1 | West |
6 | 5 | RecursiveTown |
7 | 5 | Tableton |
UserContainerRoles
ID | ContainerID | RoleID | UserID |
---|---|---|---|
1 | 1 | 1 | 1 |
1 | 1 | 2 | 2 |
1 | 2 | 1 | 2 |
1 | 6 | 3 | 3 |
So - how can we, succinctly, get a list of the containers and roles that apply to a particular user?
Enter the recursive CTE. Now, recursive CTEs can be a bit interesting. They can also suck, performance-wise. But, for smaller data-access patterns, such as this one, they can provide some very flexible solutions to some problems that previously were not easy to solve at all.
Recursive CTEs follow a simple pattern, but it can seem a bit daunting at first. They all follow this pattern:
Anchor
UNION
Recursor
The Anchor is the statement that returns the basic result set - the first level of recursion. This is the result set that we start with. In the example above, it is the containers to which the user has been given a direct role assignment through the UserContainerRoles table.
The Recursor is the interesting part - it's a query that references the Anchor, and provides the next level of result set. Very nice, but what happens next? Well, next the result set that was last returned by the recursor is passed to the recursor again - and this loop continues until either the recursor part returns no more rows, or the maxrecursion
limit is reached.
Now I'm assuming that the fact that you're still reading means you probably just want to see the code. Who am I to stand in your way?
CREATEFUNCTION[dbo].[fn_GetContainerRolesForUserID](@UserIDint) RETURNSTABLEASRETURN (WITH Hierarchy (ContainerID, RoleID, HierarchyLevel) AS (SELECT [c].[ID], [ucr].[RoleID], 1AS HierarchyLevel FROM[dbo].[Containers]AS [c] INNERJOIN[dbo].[UserContainerRoles]AS [ucr] ON [ucr].[ContainerID] = [c].[ID] WHERE [ucr].[UserID] =@UserIDUNIONALLSELECT [c].[ID], [h].[RoleID], [h].[HierarchyLevel] +1AS HierarchyLevel FROM[dbo].[Containers]AS [c] INNERJOIN [Hierarchy] AS [h] ON [c].[ParentContainerID] = [h].[ContainerID]) SELECT [ContainerID], [Name], [RoleID] FROM (SELECT [ContainerID], [Name], [RoleID], [HierarchyLevel], ROW_NUMBER() OVER (PARTITIONBY [h].[ContainerID] ORDERBY [HierarchyLevel]) AS __RN FROM [Hierarchy] AS [h] INNERJOIN[dbo].[Containers]AS [c] ON [h].[ContainerID] = [c].[ID]) AS iDat WHERE [__RN] =1)
The only non-obvious thing about this query is the use of the ROW_NUMBER
function. This is what causes a permission defined at a lower level to take precedence over one defined at a higher level - this is because HeirarchyLevel
increases each time it passes through the recursor section of the query, so we want to choose the container role with the lowest hierarchy level for each container.
So, let's look at each of our results:
SELECT [ContainerID], [Name], [RoleID] FROM[dbo].[fn_GetContainerRolesForUserID](1)
ContainerID | Name | RoleID |
---|---|---|
1 | Company A | 1 |
2 | North | 1 |
3 | South | 1 |
4 | East | 1 |
5 | West | 1 |
6 | RecursiveTown | 1 |
7 | Tableton | 1 |
Ok, so Bob has the admin role at all levels - just what we wanted to see...
SELECT [ContainerID], [Name], [RoleID] FROM[dbo].[fn_GetContainerRolesForUserID](2)
ContainerID | Name | RoleID |
---|---|---|
1 | Company A | 2 |
2 | North | 1 |
3 | South | 2 |
4 | East | 2 |
5 | West | 2 |
6 | RecursiveTown | 2 |
7 | Tableton | 2 |
James has the Admin role for North, but the Reporter role everywhere else - again, just what we want...
SELECT [ContainerID], [Name], [RoleID] FROM[dbo].[fn_GetContainerRolesForUserID](3)
ContainerID | Name | RoleID |
---|---|---|
6 | RecursiveTown | 3 |
This just shows that Tim has the Engineer role at RecursiveTown only - excellent...
Simple, right? Happy T-SQL Tuesday!