SQL Server : Storing Hierarchical ACL Data - sql-server

I would like implement a database containing hierarchical acl data
My tables
USERS: idUser,username,...
GROUPS: idGroups,name...
GROUPSENTITIES: idGroup, idChild, childType (1 for users, 2 from groups)
ROLES : idRole,name...
ROLESENTITIES: idRole, IsDeny, idChild, childType (1 for users, 2 from groups)
Every user can belong to 0 or more groups
Every group can belong to 0 or more groups
Every user and every group can belong to 0 or more roles and roles can be allowed or denied
If an explicit deny is found, role is denied
How can I store this kind of data? Is my design correct?
Is it possible retrieve a list of users with all allowed roles?
Can you please write me a query (T-SQL based) for extract this information from db
thanks in advance

You can write the tables as you would expect. For instance, to get all the users in a group, when there are hierarchical groups, you would use a recursive CTE. Assume the group table is set up as:
create table groups (
groupid int,
member_userId int,
member_groupid int,
check (member_userId is NULL or member_groupid is null)
);
with usergroups as (
select groupid, member_userid, 1 as level
from groups
union all
select g.groupid, users.member_userid, 1+level
from users u join
groups g
on u.member_groupid = g.groupid
)
select *
from usergroups;

Related

Simple user permissions database structure

I'm looking for a right solution for database structure regarding user permissions.
Tables:
users
companies (relevant columns: id)
projects (relevant columns: id,company_id)
jobs (relevant columns: id, company_id,project_id)
Scenarios I want to accomplish is to have specific user and/or users assigned to:
all the projects within company ("Cindy is assigned to all projects and all jobs within company")
select projects within company ("Cindy is assigned to three out of five projects and is assigned to all jobs within those three projects")
selected job(s) within project(s) ("Cindy is assigned to five jobs out of ten within one project and two jobs within the other project")
I think about separate permissions table where I just insert permissions to relevant jobs and to use the relevant columns from jobs table to cascade permissions upwards. In other words - if a user has permission for a specific job then it also has permission for parent project and parent company.
SQL Fiddle: http://sqlfiddle.com/#!9/74a4d3/2
Here is a proposed table structure for permissions:
USER_ID OBJ_TYPE OBJ_ID PERMISSION
JDOE COMPANY 1 1-READONLY
JDOE COMPANY 2 2-READWRITE
JDOE PROJECT 1 2-READWRITE
Then code to check user access could look something like:
SELECT MAX(permission) FROM permissions
WHERE user_id = :USERID
AND ( (obj_type = 'JOB' and obj_id = :JOBID)
OR (obj_type = 'PROJECT' and obj_id = :PROJECTID)
OR (obj_type = 'COMPANY' and obj_id = :COMPANYID))

Find Orphaned Folders using CTE

I am trying to create an SQL Syntax to search my database for folders on the network that users can't access.
Let me explain:
My Database has 5 tables:
Table: Folders
FUID - INT - Unique ID
Path - Varchar - Example: E:\shared
STATUS - INT - 1 = good, 0 = stale
Table: Groups
GRUID - INT - Unique ID
Class - varchar - User or Admin Group (admin group being a sysadmin group)
Name - Example HR_Users
STATUS - INT - 1 = good, 0 = stale
Table: Users:
URUID - INT - Unique ID
Class - varchar - Standard User or Admin User (admin user being a sysadmin)
Name - Varchar - Example: Smith, John
STATUS - INT - 1 = good, 0 = stale
Table: UG_JOIN:
UID: Unique ID
GRUID - INT - Relationship to Groups
URUID - INT - Relationship to Users
STATUS - INT - 1 = good, 0 = stale
Table: ACLS:
UID - INT - Unique ID
FUID - INT - Relationship to Folders FUID
GRUID - INT - Relationship to Group GRUID
URUID - INT - Relationship to User URUID
ACCESS - VARCHAR - Type of Access, list, read, modify, full control
STATUS - INT - 1 = good, 0 = stale
The reason I have to have both the users and groups in the ACL table is because some users are directly assigned permissions at the folder, not by group.
Anyway, What I am trying to find out is:
What folders don't have any permissions for standard users
Including the above, I want to be able to filter out by ACCESS, so search for folders where Standard Users don't have Modify Access).
Also want to filter out stale groups, users, UG_Join, folders and acls using the STATUS Column
I'm NOT trying to find what certain users have access to. I don't care about that, what I want is what folders standard users cant access (list, read, modify, full control). I don't expect to see usernames or group names in my result, just paths.
Logically all I can come up with is get all the paths that Admins have access to (which is 100% of the paths) then skip paths that end-users have access two, leaving only paths that only Admins have access to. Any idea on how I would go about this? Thanks!
Please try the query below. It gets all the folders, and then uses all standard users and any standard groups of that user before applying to filter to only show the folders without a standard user or a standard user in a standard group assigned.
SELECT DISTINCT
F.Path
FROM Folders F
INNER JOIN ACLS A
ON A.FUID = F.FUID
LEFT OUTER JOIN (
SELECT
U.URUID,
G.GRUID
FROM Users U
LEFT OUTER JOIN UG_JOIN UG
ON UG.URUID = U.URUID
LEFT OUTER JOIN Groups G
ON G.GRUID = UG.GRUID
AND G.Class = 'User' -- Whatever class needs to be user group
WHERE U.Class = 'Standard User' -- Whatever class neeed to be standard group
-- AND U.STATUS = 1 -- If you only care about active users
) UG
ON (A.URUID = UG.URUID OR A.GRUID = UG.URUID)
WHERE UG.URUID IS NULL
not sure how to combine first and second requirement, so i use Or operator at below, lets me know if i misunderstand u :)
DECLARE #Class NVARCHAR(100) = 'STANDARD USER'
DECLARE #Access NVARCHAR(100) = 'Modify'
SELECT F.[PATH]
FROM Folders F
INNER JOIN ACLS A
ON A.FUID = F.FUID
INNER JOIN Users U
ON U.URUID = A.URUID
INNER JOIN Groups G
ON G.GRUID = A.GRUID
INNER JOIN UG_JOIN UG
ON UG.URUID = U.URUID
INNER JOIN UG_JOIN UG2
ON UG2.GRUID = G.GRUID
WHERE (U.Class != #Class --First requirement : What Folders dont have any permission for standard user
-- means i will filter out standuser
OR (A.Access Not LIKE '%' + #Access + '%'
AND U.Class = #Class)-- second requirement: search for folders where standard users dont have modify access
)
AND F.Status != 0 --Third reqirement : also want to filter out stale group for all the table
AND A.Status != 0
AND U.Status != 0
AND G.Status != 0
AND UG.Status != 0
AND UG2.Status != 0

SQL Server: transforming query for materialized path traversing in a view

I would like to transform this query in a view that I can call passing the UserId as a parameter to select all groups (including subtrees of those groups) related to that user.
In fact I have 2 tables:
"EVA_Roles" which contains the roles tree, defined through a materialized path in the RoleHid field as varchar(8000)
"EVA_UsersInRoles" which related users to roles through the field UserId
Problem here is that only some roles may be related to the user in the EVA_UsersInRoles table, but maybe those roles are parents to other roles in the tree hierarchy so I have to retrieve multiple subtrees for each user.
Finally I came up with this query which seems to work fine, but I would like to transform it in a View. The problem I'm facing of course is that the UserId parameter, which is the one I would use to filter the view results, is inside the subquery.
Any hint to refactor this into a view?
SELECT A.RoleId, E.EndDate FROM EVA_Roles A INNER JOIN EVA_Roles B ON
A.RoleHid LIKE B.RoleHid + '%' AND B.RoleHid IN (SELECT RoleHid FROM EVA_Roles C
LEFT JOIN EVA_UsersInRoles D ON C.RoleId = D.RoleId WHERE
(D.Userid = #0 OR C.RoleId = #1) AND C.ApplicationId = #2)
LEFT JOIN EVA_UsersInRoles E ON A.RoleId = E.RoleId AND E.UserId = #0 WHERE
A.ApplicationId = #2 ORDER BY A.RoleId
I left parameters where I should pass values to the view. I think it may be impossible to refactor in a view. It was just to exploit my micro-ORM (PetaPoco) in a more friendly way, otherwise I have to use the SQL in my code but it's ok, don't loose your mind on this.
About the tables definition:
EVA_Roles
RoleId INT - Primary Key
RoleHid VARCHAR(800) - Here I store the materialized path of the tree using nodes
ids... An example on this later.
RoleLevel INT - Security level of the role
RoleName INT - Name of the role (admin, guest, ...)
ApplicationID INT - Id of the application (in a multi app scenario)
EVA_UsersInRoles
RoleId - INT
UserId - INT
The materialized path in RoleHid follows this logic. Consider this data where RoleId 2 is child of RoleId 1:
RoleId 1
RoleHid "1."
RoleId 2
RoleHid "1.2."
With the query above I'm able to retrieve all subtrees tied to a specific user and application.

Adding Customer Specific Record in Master Table

I have the following table designed already.
Now the new requirement is i need to show some Category for all the
tenants. Also each tenant should able to add their new Category into
the Master Category. So they can see all master + their specific
categories
Tenant Table
TenantId
Name
Group Table
GroupId
Name
Category Table
CategoryId
Name
TenantXCategory
TenantId
CategoryId
What changes i can do in above tables to achieve it? I tried this below
Modified Category table to below.
Category Table
CategoryId
Name
TenantId NULL // This indicates tenant specific category
Add a unique key for TenantId and Name
Then queried
SELECT *
FROM Category where TenantId = 1
UNION
SELECT *
FROM Category where TenantId IS NULL
But the problem is if two tenant only want to see a particular
Category, I need to add a new row with other TenantId in Category
table. This mean i am creating duplicate entry in a lookup table. Any
suggestion to achieve the new requirement?
So, a tenant A may see:
(1) master categories,
(2) categories that belong to tenant A,
(3) categories which belong to another tenant and which the other tenant explicitly allowed tenant A to see
Your present schema seems to satisfy the requirements. In particular, the first two rules can indeed be implemented using a nullable TenantId column in the Category table, where NULL would stand for a master category and a non-NULL value would reference the creator/owner of the category and thus signify a tenant-specific category. (I might rename the column to something like OwnerTenantId for better clarity, but that might be just me.)
To retrieve only master categories or those that belong to the specified tenant, you could use the query you've posted in your question or this one (which will probably yield an identical execution plan to your query's):
SELECT
CategoryId,
CategoryName,
CASE
WHEN TenantId = #TenantId THEN 'Mine'
WHEN TenantId IS NULL THEN 'Master'
END AS Ownership
FROM Category
WHERE TenantId = #TenantId
OR TenantId IS NULL
;
To implement the third rule, you could use your TenantXCategory table to store categories available to tenants in addition to those that are accessible using the first two rules. That is, if tenant M permits tenant N to see some of tenant M's categories, the categories' Ids would be inserted into TenantXCategory along with tenant N's Id.
So, to query all categories available to a particular tenant, you could do something along the lines of the following:
SELECT
c.CategoryId,
c.CategoryName,
CASE
WHEN c.TenantId = #TenantId THEN 'Mine'
WHEN c.TenantId IS NULL THEN 'Master'
WHEN tc.CategoryId IS NOT NULL THEN 'Someone else''s'
END AS Ownership
FROM Category c
LEFT JOIN TenantXCategory tc
ON tc.CategoryId = c.CategoryId AND tc.TenandId = #TenantId
WHERE c.TenantId = #TenantId
OR c.TenantId IS NULL
OR tc.CategoryId IS NOT NULL
;
If you're going to be adding additional categories, you should probably have an identifier to distinguish between master and user-defined categories.
CREATE TABLE CategoryType (
[CategoryTypeID] int primary key identity,
[Description]
)
CREATE TABLE Category (
[CategoryID] int primary key identity,
[CategoryName] nvarchar(max)
[CategoryTypeID] int
)
/* CategoryTypes
1 Master
2 User-defined */
Show categories for a specific tenant
SELECT *
FROM TenantXCategory txc
JOIN Tenant t
ON t.TenantID = txc.TenantID
JOIN Category c
ON c.CategoryID = txc.CateoryID
WHERE t.TenantName = N'user1909604'
-- AND c.CategoryTypeID = 1 -- Only show master categories
-- AND c.CategoryTypeID = 2 -- Only show user-defined categories
-- AND c.CategoryID in (1, 2) -- Only show specific categories
To add a category for a user, store the category in your cross reference table. If the category doesn't exist, you'll have to add it to the category table first.
INSERT TenantXCategory (TenantID, CategoryID)
SELECT #TenantID, #CategoryID
--
Unless you're storing who created the category (for auditing purposes), this should work. If not, I misunderstood your question and you should clarify what you're trying to do.

SaaS- Tenant Specific Lookup Data in Shared Database

I am developing multitenant SaaS based application and going with Shared Database for storing all the tenant records with help of TenantId column.
Now the problem is i have some list of lookup records that needs to be shared for all the tenants. For example list of games.
GamesTable
Id
GameName
Also have another table used for storing only tenant specific records
TenantGames
Id
TenantId
GameName
The basic need is i want to use both table data and get the necessary details (Game_Name) while joining with another transaction table like UserGames. How can i achive this with this design? Here Game_Name can be either referred from Games Shared table or TenantSpecificGames table
Is there any other DB design which allows me to do mix both common master data and tenant master data with JOIN?
Basic requirement is keep common data and allow customization for the tenants if they want to add any new items.
This is the design I would then use.
Games
Id
GameName
IsTenantSpecific
SomeGameSpecificColumn
TenantGames
GameId
TenantId
SomeTenantSpecificColumn
AnotherTenantSpecificColumn
Then you can query that table in a Join with:
...
FROM
Games
INNER JOIN UserGames ON
UserGames.GameId = Games.Id
LEFT JOIN TenantGames ON
TenantGames.GameId = Games.Id
WHERE
TenantGames.TenantId = #tenantId OR
(
TenantGames.TenantId IS NULL AND
IsTenantSpecific = 0
)
Game specific fields can be put in the Games table. Tenant specific fields can be added to the TenantGames table, and those fields will be NULL if it is not a tenant specific customization.
We have a saas based database and we keep common data and tenant data in the same table.
Concept
GamesTable
Id NOT NULL
TenantId NULL
GameName NOT NULL
Add a unique key for TenantId and GameName
if TenantId is NULL you know it is common data
if TenantId is NOT NULL you know it belongs to a specific tenant and who exactly.
"Is there any other DB design which allows me to do mix both common
master data and tenant master data with JOIN?"
Yes
SELECT *
FROM GamesTable where TenantId = 'your tenant id'
UNION
SELECT *
FROM GamesTable where TenantId IS NULL -- common
This is a classic example of "many to many".
Table: Games
------------
GameID
GameName
IsMasterGame
TennantGames
------------------
GameID
TennantID
Tennants
------------
TennantID
...
To get the games for a given tennant, you would run a query like:
select *
from Games
where isMasterGame = true
union
select *
from Games g,
TennantGames tg
where g.GameID = tg.GameID
and isMasterGame = false
and tg.TennantID = $currentTennant
(Apologies for archaic join syntax)
The union allows you to ask two questions: which games apply to everyone (isMasterGame = true), and secondly which games apply to the current tennant (tg.TennantID = $currentTennant). Logically, tennant games cannot also be master games.
You can merge the tables leaving TenantId as NULL for records you wish to not be Tenant specific.
Games
Id
TenantId
GameName
The you can query that table in a Join with:
...
FROM
Games
INNER JOIN UserGames ON
UserGames.GameId = Games.Id
WHERE
Games.TenantId = #tenantId OR
Games.TenantId IS NULL
This will save you the trouble of ensuring that the Id is unique between the tables, unless you are using a UNIQUEIDENTIFIER for the Id.

Resources