行级安全删除块谓词和级联删除约束问题

Row Level Security delete block predicate and cascade delete constraint issue

考虑以下 tables:

客户端 table 列:

带有列的工单 table:

附件 table 包含以下列:

考虑具有以下删除谓词功能,该功能应该允许用户仅删除附件匹配特定描述的票证:

    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' 

然后我们有应用谓词 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)

问题:即使附件描述符合安全谓词条件,也无法删除票证,因为附件记录信息不可用,在行级安全时检查 DML 是否已按照文档执行,删除级联约束已被应用,记录已被删除,但尚未提交。

注意:在应用相同条件的更新 RLS 限制的情况下,安全性工作得很好并且符合预期。

问题:如何克服这个问题并使删除策略按预期工作?

不存在工单的预估删除计划或实际删除计划:

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

表明安全功能“驻留在”两个“删除”之间: 最左边的部分是 T-SQL delete(删除语句) 右侧是工单的集群删除(和附件的级联删除)

安全功能是“执行和评估”[嵌套循环的内部]行是removed/deleted[嵌套循环的外部]但是before 行的删除被提交 [t-sql delete].

高级描述可以是:对于每个工单行,删除工单和相应的附件,输出删除的外部 key:ticket_guid(序列运算符,由底部 TableSpool 提供)并为每个已删除的 guid(嵌套循环,外部)执行安全功能(嵌套循环的内部)左半连接,因为安全功能可能 return 没有结果,evaluate/assert 如果函数的结果为空或不为空,如果不为空,工单行将被“删除”并移至下一张工单。

如果安全函数引用了行被删除的任何 tables,那么这些 tables 将被重新访问,但是在删除行之后,并且如果函数的结果取决于已删除的行则安全性将始终失败。

这可以用一个函数来测试,该函数检查假定的“待删除”(实际上它之前已被删除)ticket_guid 是否存在:

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

由于函数是在行删除后计算的,被删除的ticket_guid不存在,函数return什么都没有,安全检查总是失败:

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

某些行被删除的事实(直到违反安全性并且回滚整个语句)可以使用调整后的安全功能进行测试:

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 ?

没有脏读。事实上,如果脏读是可能的,安全功能可能会起作用:

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 

预期被颠覆,因为 create security policy 语句有点误导。
更准确的说法是:

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

一般来说,最好以确定性的方式处理 RLS,即:根据常量、不受 DML 操作影响的值评估安全函数。在某种程度上,安全性本质上是确定性的。如果安全功能依赖于其他 tables(或 columns/values 而不是 fixed/persisted*),那么它就变得不确定(和不可预测的 table),但最终结果,安全策略评估,必须是确定性的。

*您可以尝试在票证 table 上创建一个计算列来检查 attachment_description = 'some specific description' 的存在并在安全功能中使用该列,但仍然如此列(不持久化,因为它访问数据)是在删除票证和附件行之后计算的,并且它对于按照您设想的方式执行策略没有任何用处。

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}'.

你想想这个需求,跟安全无关==谁可以删除数据,跟数据的完整性有关,根据你的业务规则:when/under什么情况下可以删除数据.如果您想将此逻辑封装在 dbschema 中,那么附件触发器是执行它的最佳位置。