In this case, Michael J Swart is hosting this month’s blog party and he asks us to write about something to do with SQL Server 2016. Read the rules at his invitation.
Row Level Security
I’ve wanted this feature to be easy for a long time. In fact, I’ve implemented a similar system a few times in different applications, but it’s been a cumbersome feature to meet, plus each developer needs to understand how the system works for it to work well. Even in the case where we once used views to hide our RLS, it was a performance issue.
Microsoft has made things easier with their Row Level Security feature. This was actually released in Azure in 2015, but it’s now available in SQL Server 2016 for every on premise installation as well.
Essentially for each row, there is some data value that is checked to determine if a user has access. This doesn’t mean a join. This doesn’t mean you write a lot of code. The implementation is simple, and straightforward, and I like it.
Security Predicate Functions
The one piece of code you need is an inline table valued function (iTVF) that returns a 1 for the rows that a user should see. You need to have some way to match up a row with a user, and that can be tricky, but if you identify a row, even in another table, you can use it.
For example, I have this table.
CREATE TABLE OrderHeader
OrderID INT IDENTITY(1, 1)
, Orderdate DATETIME2(3)
, CustomerID INT
, OrderTotal NUMERIC(12, 4)
, OrderComplete TINYINT
, SalesPersonID INT
There’s nothing in this table that really helps me identify a user that is logged into the database. However, I do have a mapping in my SalesPeople table.
CREATE TABLE SalesPeople
SalesPersonID INT IDENTITY(1, 1)
, SalesFirstName VARCHAR(200)
, SalesLastName VARCHAR(200)
, username VARCHAR(100)
, IsManager BIT
Granted, this could mean some change of code, but perhaps you can somehow use a user name in tables to query AD or other directory and map this to a user name.
Once I have that mapping, I’m going to create a function. My function will actually look at the SalesPeople table, and map the parameter passed into the function to the value in the table.
CREATE FUNCTION dbo.RLS_SalesPerson_OrderCheck ( @salespersonid INT )
1 AS [RLS_SalesPerson_OrderCheck_Result]
@salespersonid = sp.SalesPersonID
OR sp.IsManager = 1
AND USER_NAME() = sp.username;
In the function, I look at the USER_NAME() function and compare that to a value in the table. This is in addition to checking the SalespersonID column.
I can use a Security Policy to bind this function to my OrderHeader table as shown here:
CREATE SECURITY POLICY dbo.RLS_SalesPeople_Orders_Policy
ADD FILTER PREDICATE dbo.RLS_SalesPerson_OrderCheck(salespersonid)
This sets the function, passing in a column from the OrderHeader table, which is the column I want evaluated in the function.When I now query the OrderHeader table, I get this:
There is data in the table. However, I don’t get rights by default, even as dbo. My USER_NAME() doesn’t match anything in the table, therefore no SalesPersonID matches. However, for other users, it works.
There is a lot more to the RLS feature, but I think it’s pretty cool and it’s something that will be highly used in many applications moving forward, especially those multi-tenant systems.
Go ahead, get the free Developer Edition and play around with RLS.