Row Level Security delete block predicate and cascade delete constraint issue

我的梦境 提交于 2021-01-07 06:31:16

问题


Consider having the following tables:

Client table with columns:

  • client_guid(uniqueidentifier - primary key)
  • client_description

Ticket table with columns:

  • ticket_guid(uniqueidentifier - primary key)
  • ticket_client_guid(uniqueidentifier - foreign key to client table)
  • ticket_description.

Attachment table with columns:

  • attachment_guid(uniqueidentifier - primary key)
  • attachment_ticket_guid(uniqueidentifier - foreign key to ticket table with ON DELETE CASCADE constraint)
  • attachment description

Consider having the following delete predicate function which is supposed to allow users to delete only tickets which attachment match specific description:

    CREATE FUNCTION [Security].[fn_ticket_delete](@ticket_guid AS SYSNAME) 
returns TABLE 
WITH schemabinding 
AS 
    RETURN 
      SELECT 1 AS fn_securitypredicate_result 
      WHERE  (SELECT attachment_description 
              FROM   dbo.attachment 
              WHERE  attachment_ticket_guid = @ticket_guid) = 'some specific attachment description' 

Then we have the security policy applying the predicate fn:

CREATE SECURITY POLICY [Security].[ticket_security] 
ADD BLOCK PREDICATE [Security].[fn_ticket_delete]([ticket_guid]) ON [dbo].[ticket] BEFORE DELETE
WITH (STATE = ON, SCHEMABINDING = ON)

Problem: Not being able to delete the ticket even if it's attachment description matches the security predicate condition because the attachment record information not being available, at the point when row level security checks the DML has already been executed as per docs and the delete cascade constraint already being applied and the record being deleted although yet not commited.

Note: The security works perfectly fine and as expected in the case where there's a update RLS restriction with same condition being applied.

Question: How can I overcome this problem and make the delete policy working as expected ?


回答1:


The estimated plan of a deletion or the actual plan of a deletion for a non-existent ticket:

delete from ticket where ticket_guid = newid()
--or
delete from ticket where ticket_guid is null

shows that the security function "resides" between two "deletions": on the left most part is the T-SQL delete (the delete statement) and on the right side the clustered deletion(s) of ticket (& cascade delete of attachments)

The security function is "executed&evaluated" after [inner part of the nested loops] the row is removed/deleted [outer part of nested loops] but before the deletion of the row is committed [t-sql delete].

A high level description could be: for each ticket row, delete the ticket and the corresponding attachment(s), output the deleted foreign key:ticket_guid (sequence operator, fed by the bottom TableSpool) and for each deleted guid (nested loops, outer part) execute the security function (inner part of nested loops) left semi join because the security function could return no results, evaluate/assert if the result of the function was null or not null, if not null the ticket row is "deleted" and move to the next ticket.

If the security function references any of the tables whose rows get deleted, then those tables are accessed anew but after the row removal and if the result of the function depends on the rows which had been deleted then the security will always fail.

This can be tested with a function which checks whether the presumed "to-be-deleted" (when in fact it is deleted before) ticket_guid exists or not:

create table dbo.ticketX
(
    ticket_guid uniqueidentifier default(newsequentialid()) primary key clustered,
    ticket_client_guid uniqueidentifier,
    ticket_description varchar(10)
); 
go
insert into dbo.ticketX(ticket_client_guid, ticket_description)
select v.guid, v.descr
from 
(values (newid(), 'ticketA'), (newid(), 'ticketB'), (newid(), 'ticketC')) as v(guid, descr)
go
insert into dbo.ticketX(ticket_client_guid, ticket_description)
select v.guid, v.descr
from 
(values (newid(), 'abc'), (newid(), 'ticketE'), (newid(), 'ticketF')) as v(guid, descr)
go    

create function dbo.fn_ticketX_delete(@ticket_guid uniqueidentifier)
returns table 
with schemabinding 
as 
return
( 
    select 1 as fn_securitypredicate_result 
    where exists(select 1 from dbo.ticketX where ticket_guid = @ticket_guid)
)
go  

create security policy dbo.ticketX_security
add block predicate dbo.fn_ticketX_delete(ticket_guid) on dbo.ticketX before delete
with (state=on, schemabinding=on);
go

Since the function is evaluated after the row deletion, the deleted ticket_guid does not exist, the function returns nothing and the security check always fails:

--The attempted operation failed because the target object 'xyz.dbo.ticketX' has a block predicate that conflicts with this operation.
delete from dbo.ticketX;

The fact that some rows get deleted (until the security is violated and the whole statement is rolledback) can be tested with an adjusted security function:

drop security policy dbo.ticketX_security;
go
--allow deletion of tickets with description like ticket%
create or alter function dbo.fn_ticketX_delete(@ticket_description varchar(10))
returns table 
with schemabinding 
as 
return
( 
    select 1 as fn_securitypredicate_result 
    where @ticket_description like 'ticket%'
    --where exists(select 1 from dbo.ticketX where ticket_guid = @ticket_guid)
)
go  
create security policy dbo.ticketX_security
add block predicate dbo.fn_ticketX_delete(ticket_description) on dbo.ticketX before delete
with (state=on, schemabinding=on);
go


delete from dbo.ticketX
output deleted.* --some rows "get deleted"

Question: How can I overcome this problem with dirty reads and make the delete policy working as expected ?

There are no dirty reads. In fact, the security function might have worked if dirty reads were possible:

RETURN 
  SELECT 1 AS fn_securitypredicate_result 
  WHERE  (SELECT attachment_description 
          FROM   dbo.attachment with(nolock) --if this were possible/enforced, it could work ??
          WHERE  attachment_ticket_guid = @ticket_guid) = 'some 

The expectations are subverted because the create security policy statement is slightly misleading.
A more accurate statement would be:

CREATE SECURITY POLICY xyz... ADD BLOCK PREDICATE... after row is deleted BEFORE DELETE commits;

In general, it is best to approach RLS in a deterministic way, that is: evaluate the security function on constants, on values which are not affected by DML actions. In a way security is deterministic by its nature. If the security function relies on other tables (or on columns/values which are not fixed/persisted*) then it becomes nondeterministic (and unpredictable) but in the very end the result, the security policy evaluation, has to be deterministic.

*You could try to create a computed column on the tickets table which checks the existence of attachment_description = 'some specific description' and use that column in the security function, but still this column (not persisted, since it accesses data) is computed after the deletion of ticket&attachment rows and it would not be of any use for enforcing the policy the way you envision it.

Lets consider that the ticket could be deleted only if there's an attachment linked to it where the attachment author basically says in the description: 'Please delete my ticket {signature}'.

If you think about the requirement, it is not related to security==who can delete data, it is related to data integrity according to your business rules: when/under what conditions it is possible to delete data. If you would like to encapsulate this logic in the dbschema then a trigger on attachments is the best place to enforce it.



来源:https://stackoverflow.com/questions/65290954/row-level-security-delete-block-predicate-and-cascade-delete-constraint-issue

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!