-6

I am trying to implement RLS on a Table (fin.RLS_Data) with following Function. I have another table stg.UserAccess to control who can access the Data Table (RLS_Data).

I am testing this on

Microsoft SQL Server 2022 (RTM-CU5) (KB5026806) - 16.0.4045.3 (X64)
    May 26 2023 12:52:08
    Copyright (C) 2022 Microsoft Corporation
    Developer Edition (64-bit) on Linux (Ubuntu 20.04.6 LTS) <X64>

UserAccess :

enter image description here

RLS_Data :

enter image description here

Following function works perfectly for readonly access users.

CREATE FUNCTION rls.fn_Data_Predicate
         (@DataProviderID AS INT, @TableName sysname)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
    SELECT 1 as Result
    FROM [fin].[RLS_Data] d
    INNER JOIN [stg].[UserAccess] e ON e.DataProviderId = d.DataProviderID
    WHERE d.DataProviderID = @DataProviderID
        AND e.[User Email] = CURRENT_USER
        AND e.TableName = @TableName 
        AND e.isAuthorized = 1
);

I want to have some exception where admin user (isAdmin) can have full access to the Data Table (RLS_Data).

user2 who is admin user should have full access to Data Table (RLS_Data), but user1 need to have access to data for DataProviderId = 10 in Data Table (RLS_Data)

I tried following

SELECT 1 as Result
FROM [fin].[RLS_Data] d
INNER JOIN [stg].[UserAccess] e ON e.DataProviderId = d.DataProviderID
WHERE e.[User Email] = CURRENT_USER 
    AND ( e.isAdmin = 1 
        OR (d.DataProviderID = @DataProviderID
            AND e.TableName = @TableName  
            AND e.isAuthorized = 1
            )
        )

This still isn't returning data for all DataProviderID if user has isAdmin set to true because of INNER JOIN [stg].[UserAccess] e ON e.DataProviderId = d.DataProviderID

I am open to solution where I have to hardcode the admin users inside function. Looking for a function which can do following , nut not sure how to implement this in function

IF CURRENT_USER IN ('dbo', 'DBENTPBIPROD')
    SELECT 1 as Result
    FROM [fin].[RLS_Data] d
ELSE
    SELECT 1 as Result
    FROM [fin].[RLS_Data] d
    INNER JOIN [stg].[UserAccess] e ON e.DataProviderId = d.DataProviderId
    WHERE d. DataProviderID = e.DataProviderId
        AND e.[User Email] = CURRENT_USER
        AND e.TableName = 'RLS_Data'
        AND e.isAuthorized = 1

Any better way to do this ?

roy
  • 6,344
  • 24
  • 92
  • 174
  • 3
    Please do note [delete](https://stackoverflow.com/q/76940421/2029983) and repost questions... As a user with >6k reputation you should know there is a [edit](https://stackoverflow.com/posts/76940421/edit) feature where you can send your question to the review queue for reopening. You waited **2 minutes** after it was sent to the review (13:38:54Z) before you deleted it (13:40:56Z); it takes longer than that for a question to be reviewed. – Thom A Aug 21 '23 at 13:49
  • I think the problem is that @OP withhold the actual test data, so we're forced to guess the relations of the tables, this over multiple questions. I vote on closing this question until this is fixed – siggemannen Aug 21 '23 at 14:13
  • I also think OP has some incorrect assumptions about how RLS works. Maybe they're trying to test the function by querying it manually _outside of RLS_, instead of testing by actually implementing the solutions that have been proposed? – Stuck at 1337 Aug 21 '23 at 14:28
  • 3
    [Please do not upload images of code/data/errors when asking a question.](//meta.stackoverflow.com/q/285551)... Please provide DDL and DML. – Thom A Aug 21 '23 at 14:55

1 Answers1

2

You only need to check the relation by provider if the user is not an admin, so you can say this without touching the RLS_Data table again (since the function gets that DataProviderId from the predicate, right?):

SELECT 1 as Result
FROM stg.UserAccess AS u
WHERE u.[User Email] = CURRENT_USER
  AND
  (
    (
      /* admin can see everything regardless of provider: */
      u.IsAdmin = 1 
    )
    OR
    (
      /* only certain users are authorized: */
      u.DataProviderId = @DataProviderId
      AND u.TableName = @TableName
      AND u.isAuthorized = 1
    )
  );

I've validated that this works. First, I created three logins and added them to a new database:

CREATE LOGIN bob_the_admin   WITH PASSWORD = 'boo', CHECK_POLICY = OFF;
CREATE LOGIN jim_the_peon    WITH PASSWORD = 'boo', CHECK_POLICY = OFF;
CREATE LOGIN ted_the_outcast WITH PASSWORD = 'boo', CHECK_POLICY = OFF;
GO
CREATE DATABASE foo;
GO
USE foo;
GO
CREATE USER bob_the_admin;
CREATE USER jim_the_peon;
CREATE USER ted_the_outcast;
GO
ALTER ROLE db_datareader ADD MEMBER bob_the_admin;
ALTER ROLE db_datareader ADD MEMBER jim_the_peon;
ALTER ROLE db_datareader ADD MEMBER ted_the_outcast;

Then created these tables and populated them with data:

CREATE TABLE dbo.RLS_Data
(ProductId int, DistributorId int, DataProviderId int);

INSERT dbo.RLS_Data VALUES(54069,10000001,10),(50444,10000002,40),
      (86603,10000003,10),(44753,10000004,40),(86589,10000005,10);

CREATE TABLE dbo.UserAccess
(
  Id int identity(1,1),
  [User Email] sysname,
  DataProviderId int,
  TableName sysname,
  isAuthorized bit,
  isAdmin bit
);

INSERT dbo.UserAccess VALUES
('bob_the_admin', 10, 'RLS_Data', 1, 1),
('jim_the_peon',  40, 'RLS_Data', 1, 0);

Then created this function and security policy:

CREATE OR ALTER FUNCTION dbo.FN_RLS_LetThemIn
(
    @DataProviderId int,
    @TableName sysname
)
RETURNS TABLE WITH SCHEMABINDING
AS RETURN 
   (
     SELECT 1 as Result
       FROM dbo.UserAccess AS u
       WHERE u.[User Email] = CURRENT_USER
       AND
       (
         (
           u.IsAdmin = 1 
         )
         OR
         (
           u.DataProviderId = @DataProviderId
           AND u.TableName = @TableName
           AND u.isAuthorized = 1
         )
       )
   );
GO

CREATE SECURITY POLICY RLS_LetThemIn
    ADD FILTER PREDICATE dbo.FN_RLS_LetThemIn(DataProviderID, N'RLS_Data')
    ON [dbo].[RLS_Data]
    WITH (STATE = ON);

When running as bob the admin, they get all the rows:

Results for bob_the_admin

When running as jim the peon, they just get 40:

Results for jim_the_peon

And when running as ted the outcast, they get nothing:

Results for ted_the_outcast

Same results for ted the outcast if I insert this row (explicitly not authorized):

INSERT dbo.UserAccess VALUES
('ted_the_outcast', 10, 'RLS_Data', 0, 0);

Please actually try implementing this before responding with comments that are really hard to understand, like "I want data from [fin].[RLS_Data] to be returned." The function doesn't return data from that table, it merely acts as a gatekeeper to which rows the current user can access. It's right there in the first line:

SELECT 1 as Result

That's all the function returns. A constant. Stop thinking about it like a join or a function that actually returns your data. It sits there and waits for users to query data from the source table, then the function only exists to tell the query which rows that user can access.

Stuck at 1337
  • 1,900
  • 1
  • 3
  • 13