Apr 202012
 

In bigger organizations, the data in a table might be sensitive to few departments but not the others. Some people may need to rows from the table that belongs to few departments where another group of people may need to access the data belongs to another few departments in one table. Those 2 groups of people might share the data from one or few departments. Implementing such logics is not a big deal for customized applications, for instance use procedures to filter rows out. What if users access data using very genaric tools, such as SSMS, in which they can arbitrarily issue queries against table. How would you selectively return rows from a table without asking uses putting filters in their queries?

Views in SQL Server act as named queries. You can run almost all the queries in a view to perform filters, aggregates, and calculations. Views do not accept parameters. So the return set of a view cannot be adjusted by parameter(s). However, there are few ways that you can indirectly pass parameters to a view. For instance, use set context info to adjust session context information and then referece context_info() function in the view.

Returning values from system functions might be varied under different circumstances. for Instant, @@SPID will always return the current Session ID. In order satisfy the need described at very beginning, I am going to reference is_member function in views to perform the filters. This function tells whether or not current user belonging to a database role.

use tempdb
create user u1 without login
go
create role r1
go
exec sp_addrolemember 'r1', 'u1'
go
execute as user = 'u1'
go 
select IS_MEMBER('r1')
go 
revert
select IS_MEMBER('r1')
-----------
1

(1 row(s) affected)


-----------
0

(1 row(s) affected)

This is the function can be borrowed to implement row level security! Now the consideration becomes to how to create a table with ROLE information associated. This can be done by adding RoleID field to the table as an example below. An index should be created on RoleID column or Combine RoleID column and MyDataID column together as a primary key. For demo purpose, we are not considering performance matters for now.

create table MyData
(
	MyDataID int identity(1,1) primary key,
	Description varchar(100),
	RoleID int
)

In our view, we can use the RoleID column to perform filters

create view vMyData
as
select * 
from MyData
where is_member(user_name(RoleID)) = 1
go

Now let’s create 2 Roles in the database and add some records to MyData table

create role Role1;
create role Role2;
select user_id('Role1') Role1,user_id('Role2') Role2
/*Role1  Role2
------ ------
5      6

(1 row(s) affected)
*/
insert into MyData(Description, RoleID) values('value1', 6)
insert into MyData(Description, RoleID) values('value1', 5)
select * from MyData
go
/*
MyDataID    Description RoleID
----------- ----------- -----------
1           value1      6
2           value1      5

(2 row(s) affected)

*/

Now let’s retrieve the data from the view

select * from vMyData
/*
MyDataID    Description RoleID
----------- ----------- -----------

(0 row(s) affected)
*/

Nothing is returned. This is because I belongs to none of the database roles, Role1, and Role2. Now let’s create users and roles and then have a test.

grant select, insert, update on vMyData to public
create user user1 without login;
create user user2 without login;
create user user3 without login;
create user user4 without login;
create Role Role12;
exec sp_addrolemember Role12, User4

exec sp_addrolemember Role1, User1
exec sp_addrolemember Role2, User2

exec sp_addrolemember Role1, User3
exec sp_addrolemember Role2, User3

exec sp_addrolemember Role1, Role12
exec sp_addrolemember Role2, Role12
go
execute as user = 'user1'
select * from vMyData
/*
MyDataID    Description RoleID
----------- ----------- -----------
2           value1      5

(1 row(s) affected)

*/
revert
go
execute as user = 'user2'
select * from vMyData
/*
MyDataID    Description  RoleID
----------- ------------ -----------
1           value1       6

(1 row(s) affected)

*/
revert
go
execute as user = 'user3'
select * from vMyData
/*
MyDataID    Description RoleID
----------- ----------- -----------
1           value1      6
2           value1      5

(2 row(s) affected)

*/
revert
go
execute as user = 'user4'
select * from vMyData
/*
MyDataID    Description RoleID
----------- ----------- -----------
1           value1      6
2           value1      5

(2 row(s) affected)

*/
revert
go
--will they have the permission to access base table?
execute as user = 'user4'
select * from MyData
/*
Msg 229, Level 14, State 5, Line 2
The SELECT permission was denied on the object 'MyData', database 'test', schema 'dbo'.
*/
go
revert

I created 4 users. User1 can see the second row. user2 can see the first row and user3 and 4 can see both. There are 2 ways to implement row level security for a user who can see data from different departments. One is put the user into differnt Roles respectively. The second one is to create another database role, put this role into roles for difference departments, and then put user to the newly created role as the example above. Users in ths case does not have permission to the base table. I am putting view visible to everyone but it will not lead any security issues from data reading perspective.

This is for demo purpose. Accessing this view will not take any indexes due to the function, is_member, in the where clause. When MyData table becomes huge. Performance will become problematic.

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="">

(required)

(required)

C# | HTML | Plain Text | SQL | XHTML | XML | XSLT |

This site uses Akismet to reduce spam. Learn how your comment data is processed.