tags:

views:

335

answers:

5

Given the tables:

role: roleid, name
permission: permissionid, name
role_permission: roleid, permissionid

I have a set of permissions, and I want to see if there is an existing role that has these permissions, or if I need to make a new one. Note that I already know the permissionid, so really the permission table can be ignored - I just included it here for clarity.

Is this possible to do in a SQL query? I imagine it would have to be a dynamically-generated query.

If not, is there any better way than the brute force method of simply iterating over every role, and seeing if it has the exact permissions?

Note, I'm looking for a role that has the exact set of permissions - no more, no less.

+4  A: 

You can select all roles that have the subset of permissions you are looking for. Count the number of permissions and see if it's exactly equal to the number of permissions you need:

select r.roleid 
from role r
where not exists (select * from role_permissions rp where rp.roleid = r.roleid and rp.permissionid not in (1,2,3,4)) -- id of permissions
   and (select count(*) from role_permissions rp where rp.roleid = r.roleid) = 4 -- number of permissions
Mehrdad Afshari
This assumes that there are no duplications in role_permission, but that's presumably a constraint already for that table.
JacobM
Yes, I think that's an acceptable assumption.
Mehrdad Afshari
This actually also selects the role if only a subset of permissions apply- eg, if you have a role with permissions 1,2,3,4, and you run this query it will match when you try either "1,2,3,4" or "1,2,3".
gregmac
@gregmac: The trick is the having clause which will make sure the count is equal to the number of permissions you are looking for, so a subset won't be matched.
Mehrdad Afshari
It doesn't (at least on SQL server). I think what is happening is the count is happening after the WHERE restricts the permissionids - so the count will always match the number supplied in the WHERE clause, not what is actually there.
gregmac
I worked around this with: select r.roleid from role r join role_permission rp on r.roleid = rp.roleidwhere rp.permissionid in (1,2,3,4) and (select count(*) from role_permission where roleid = r.roleid) = 4group by r.roleidBut I'm not sure if it is the best way..
gregmac
@gregmac: lemme check..
Mehrdad Afshari
@gregmac is right, but the resulting query is not really "pretty" ...
IronGoofy
@gregmac: my fault. I think this looks better.
Mehrdad Afshari
@mehrdad: I thought the other one looked better - two subqueries here, doesn't seem to be much point in what is a double negation, effectively, or am i missing something? I'm still using the query above, but i'll award answer anyway since this got me there
gregmac
@gregmac: this new one is better. subqueries are basically evaluated efficiently using join techniques. I've tested the new one and the execution plan is much better. I think it's also simpler to read as it doesn't have group by and having clauses.
Mehrdad Afshari
A: 

Maybe use a subquery along the lines of ...

SELECT * FROM role r
WHERE r.rolid = (SELECT x.roledid 
                 FROM role_permission 
                 WHERE x.permissionid in (1,2,3,4);

Sorry, haven't validated this, but having just spent an hour debugging PHP code for another question I feel the need for a glass of red wine.

James Piggot
Doesn't work. It'll match every role that has one of the permissions.
Mehrdad Afshari
You are right Mehrdad it isn't even valid SQL as it is missing a closing ")" at the end, in too much of a hurry to post a sensible answer
James Piggot
+1  A: 

This is an old sql trick (works in Oracle, at least):

SELECT roleid FROM role_permission t1
WHERE NOT EXISTS (
(SELECT permissionid FROM role_permission t2 WHERE t2.roleid = t1.roleid
 MINUS
 SELECT permissionid FROM role_permission WHERE roleid = 'Admin')
UNION
(SELECT permissionid FROM role_permission t2 WHERE roleid = 'Admin'
 MINUS
 SELECT permissionid FROM role_permsission t2 WHERE t2.roleid = t1.roleid)
)

Also not validated. Red wine always sounds good.

JK
This is pretty nice .. someone should throw this and Mehrdad's suggestion together ...
IronGoofy
Thanks. I like Mehrdad's solution as well. The advantage to this is that you don't need to know the number of permissions you are trying to match before hand, only the roleid that you are trying to match. The downsode is that it's fairly opaque.
JK
I started writing a new answer .. but basically all that is needed is to change roleid = 'Admin' into roleid IN (1,2,3,4) as in Mehrdad's answer. All that is left is explaining why this works .. ;-)
IronGoofy
For Oracle, one suggestion would be to start with a 'with reqd_roles as ..', this would make things a bit easier to understand. You'd also only need to change the required roles in one place.
IronGoofy
IronGoofy: the reason is obvious: A Δ B = Φ <=> A = B
Mehrdad Afshari
+1  A: 

You basically need to check if there is a role that has the exact number of distinct permissions as you are checking for.

I have checked this stored procedure on SQL Server 2005 and it returns only those role ids that have an exact match of permission ids to those in the passed in list of comma separated permission ids -

CREATE PROC get_roles_for_permissions (@list nvarchar(max)) -- @list is a comma separated list of your permission ids
AS
SET NOCOUNT ON

BEGIN

DECLARE     @index INT, @start_index INT, @id INT
DECLARE     @permission_ids TABLE (id INT)           

    SELECT @index = 1 
    SELECT @start_index = 1
    WHILE @index <= DATALENGTH(@list)
    BEGIN

        IF SUBSTRING(@list,@index,1) = ','
        BEGIN
                SELECT @id = CAST(SUBSTRING(@list, @start_index, @index - @start_index ) AS INT)
                INSERT INTO @permission_ids ([id]) VALUES (@id)
                SELECT @start_index = @index + 1
        END
        SELECT @index  = @index + 1
    END
    SELECT @id = CAST(SUBSTRING(@list, @start_index, @index - @start_index ) AS INT)
    INSERT INTO @permission_ids ([id]) VALUES (@id)

SELECT 
r.roleid 
FROM
role r 
INNER JOIN 
role_permission rp 
ON r.roleid = rp.roleid
INNER JOIN
@permission_ids ids
ON
rp.permissionid = ids.id
GROUP BY r.roleid
HAVING(SELECT COUNT(*) 
       FROM role_permission 
       WHERE roleid = r.roleid) = (SELECT COUNT(*) FROM @permission_ids)

END

Example Data

CREATE TABLE [dbo].[role](
    [roleid] [int] IDENTITY(1,1) NOT NULL,
    [name] [nvarchar](50)
    )

CREATE TABLE [dbo].[permission](
    [permissionid] [int] IDENTITY(1,1) NOT NULL,
    [name] [nvarchar](50)
    )

CREATE TABLE [dbo].[role_permission](
    [roleid] [int],
    [permissionid] [int]
    )

INSERT INTO role(name) VALUES ('Role1')
INSERT INTO role(name) VALUES ('Role2')
INSERT INTO role(name) VALUES ('Role3')
INSERT INTO role(name) VALUES ('Role4')

INSERT INTO permission(name) VALUES ('Permission1')
INSERT INTO permission(name) VALUES ('Permission2')
INSERT INTO permission(name) VALUES ('Permission3')
INSERT INTO permission(name) VALUES ('Permission4')

INSERT INTO role_permission(roleid, permissionid) VALUES (1, 1)
INSERT INTO role_permission(roleid, permissionid) VALUES (1, 2)
INSERT INTO role_permission(roleid, permissionid) VALUES (1, 3)
INSERT INTO role_permission(roleid, permissionid) VALUES (1, 4)
INSERT INTO role_permission(roleid, permissionid) VALUES (2, 2)
INSERT INTO role_permission(roleid, permissionid) VALUES (2, 3)
INSERT INTO role_permission(roleid, permissionid) VALUES (2, 4)
INSERT INTO role_permission(roleid, permissionid) VALUES (3, 3)
INSERT INTO role_permission(roleid, permissionid) VALUES (3, 4)
INSERT INTO role_permission(roleid, permissionid) VALUES (4, 4)

EXEC get_roles_for_permissions '3,4' -- RETURNS roleid 3
Russ Cam
+1  A: 

Having made a hash of my first answer to this question, here is a slightly left field alternative which works but does involve adding data to the database.

The trick is to add a column to the permission table that holds a unique value for each row.

This is a fairly common pattern and will give precise results. The downside is you have to code your way around hiding the numerical equivalents.

id int(10) 
name varchar(45) 
value int(10)

Then the contents will become:

Permission:           Role                   Role_Permission
id  name  value       id  name               roleid permissionid
--  ----  -----       --  ----               ------ ------------
1   Read     8         1   Admin                1          1
2   Write   16         2   DataAdmin            1          2
3   Update  32         3   User                 1          3
4   Delete  64                                  1          4
                                                2          1
                                                2          3
                                                2          4

Then each combination of roles gives a unique value:

SELECT x.roleid, sum(value) FROM role_permission x
inner join permission p
on x.permissionid = p.id
Group by x.roleid

Giving:

roleid   sum(value)
------   ----------
    1          120       (the sum of permissions 1+2+3+4 = 120)
    2          104       (the sum of permissions 1+3+4 = 104)

Now where did I leave that corkscrew...

James Piggot