Quantcast
Channel: The Insiders: Journeys in SQL - community
Viewing all articles
Browse latest Browse all 8

T-SQL Tuesday #18 - CTEs - The permission hierarchy problem

$
0
0

Recently, I was looking at a problem which involved an arbitrary tree structure for permissions. The idea was similar to NTFS security, in that the permission that was defined furthest from the root of the tree would apply to any items below it.

So, consider the following structure:

Users

  • Bob
  • James
  • Tim

Roles

  • Administrator
  • Reporter
  • Engineer

Containers

Company A
    North
    South
    East
    West
        RecursiveTown
        Tableton

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

IDName
1Bob
2James
3Tim

Roles

IDName
1Administrator
2Reporter
3Engineer

Containers

IDParentContainerIDName
1<NULL>Company A
21North
31South
41East
51West
65RecursiveTown
75Tableton

UserContainerRoles

IDContainerIDRoleIDUserID
1111
1122
1212
1633

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)

ContainerIDNameRoleID
1Company A1
2North1
3South1
4East1
5West1
6RecursiveTown1
7Tableton1

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)

ContainerIDNameRoleID
1Company A2
2North1
3South2
4East2
5West2
6RecursiveTown2
7Tableton2

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)

ContainerIDNameRoleID
6RecursiveTown3

This just shows that Tim has the Engineer role at RecursiveTown only - excellent...

Simple, right? Happy T-SQL Tuesday!


Viewing all articles
Browse latest Browse all 8

Latest Images

Trending Articles





Latest Images