行级安全删除块谓词和级联删除约束问题
Row Level Security delete block predicate and cascade delete constraint issue
考虑以下 tables:
客户端 table 列:
- client_guid(uniqueidentifier - 主键)
- client_description
带有列的工单 table:
- ticket_guid(uniqueidentifier - 主键)
- ticket_client_guid(uniqueidentifier - 客户端外键table)
- ticket_description.
附件 table 包含以下列:
- attachment_guid(uniqueidentifier - 主键)
- attachment_ticket_guid(uniqueidentifier - 工单外键table 带 ON DELETE CASCADE 约束)
- 附件说明
考虑具有以下删除谓词功能,该功能应该允许用户仅删除附件匹配特定描述的票证:
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 中,那么附件触发器是执行它的最佳位置。
考虑以下 tables:
客户端 table 列:
- client_guid(uniqueidentifier - 主键)
- client_description
带有列的工单 table:
- ticket_guid(uniqueidentifier - 主键)
- ticket_client_guid(uniqueidentifier - 客户端外键table)
- ticket_description.
附件 table 包含以下列:
- attachment_guid(uniqueidentifier - 主键)
- attachment_ticket_guid(uniqueidentifier - 工单外键table 带 ON DELETE CASCADE 约束)
- 附件说明
考虑具有以下删除谓词功能,该功能应该允许用户仅删除附件匹配特定描述的票证:
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 中,那么附件触发器是执行它的最佳位置。