SQL Server 2016 CPT3中包含了一个新特性叫Row Level Security(RLS),允许数据库管理员根据业务需要依据客户端执行脚本的一些特性控制客户端能够访问的数据行,比如,我们希望业务部的经理只能查看他所在部门的员工的薪资情况。以往像要实现这样的功能,都是要通过视图里层的逻辑编写来实现。以前某个项目就是这么实现的。或者通过在应用程序层去实现,比如在提交命令到数据库前,通过在查询语句中添加WHERE字句条件来实现数据过滤。这样显然RLS是更加简便的去实现行级别权限控制。
那么它是怎么实现的呢?结合SECURITY POLICY和内联函数
1)SECURITY POLICY作用于表,指明PREDICATE类型以及PREDICATE的参数传入;
2)PREDICATE的定义在内联函数里面;
这么理解吧。PREDICATE的中文意思就是谓语。对SQL Server查询语句执行计划了解的人对这个词肯定不会陌生。筛选条件在执行计划中的表示就是谓语(predicate)。那么这么看其实RLS的实现就是通过表中和某个或者多个字段属性作为引用(参考)条件,而内联函数的作为只不过是封装了谓语(predicate)的定义。定义本身是基于业务而定,所以有开发人员自身去决定。但是作为封装容器,还是需要有人去引用它,那么就是SECURITY POLICY啦。所以SECURITY POLICY成了表与谓语定义的间的纽带。
即:Table <- SECURITY POLICY -> Inline Function
RLS支持两种类型的PREDICATE:1)FILTER PREDICATE;2)BLOCK PREDICATE。两者的区别是什么呢?前者对数据读取操作有效,后者对增删改有效。前者是把违反了谓语的数据行过滤掉,而后者在针对违反了谓语的数据行进行增删改操作的时候触发错误。但是其实FILTER PREDICATE对于更新和删除是生效的,因为更新和删除第一步是先读取数据行嘛。
它们各自的共同点是:
1)内联函数中引用的其他表示不需要考虑是否具有查看权限的;
2)如果SECURITY POLICY的STAT=OFF,那么数据行筛选就失效了;
3)就算是dbo或者db_owner也会受到SECURITY POLICY的影响;
4)因为创建用于SECURITY POLICY的内联函数需要架构绑定,因此如果参考列被更新会触发错误,但是表中的别的栏位不会,同样的修改内联函数也会触发错误;
5)一张表中只可以对某种操作有一个谓语,比如不可以定义多个BEFORE UPDATE的BLOCK PREDICATE;
对于BLOCK PREDICATE:
1)虽然它类似于TRIGGER,但是其实两者是不一样。TRIGGER记录着修改前后的行的栏位数值,而BLOCK PREDICATE只会知道之前或者之后的值(取决于BEFORE UPDATE或者AFTER UPDATE)
2)只要违反了参考栏位的谓语的行才不可以操作;
3)对于BULK INSERT同样生效;
讲了这么多,那么其实RLS相当于用一层WHERE条件暗地里屏蔽掉了一些数据行。
微软的建议:
1)为用于RLS的内联函数和SECURITY POLICY创建单独的schema。
2)要创建SECIRITY POLICY需要有ALTER ANY SECURITY POLICY的权限。
3)避免数据类型转换而造成内联函数内就报错。
4)避免在内联函数中过多的表连接查询。
有一点需要注意:可以对视图应用RLS,但是如果应用了RLS的表被视图引用了,视图是无法创建索引视图的。
RLS和Columnstore索引、CDC、Memory-Optimized Tables是兼容的
MSDN上就给出了一个非常典型的例子,就是员工只能看到他自己的工资,而经理可以看到他下属员工的工资。
第一步先创建好用户和元数据
CREATE USER Martin WITHOUT LOGIN; CREATE USER Sara WITHOUT LOGIN; CREATE USER Amy WITHOUT LOGIN; CREATE TABLE dbo.Personnel ( EmployeeID INT, Name NVARCHAR(100), Department NVARCHAR(100), Position NVARCHAR(100), ReportTo INT, Salary FLOAT ); GO INSERT INTO dbo.Personnel ( EmployeeID, Name, Department, Position, ReportTo, Salary ) VALUES ( 1, 'Martin', 'Accounting', 'Manager', NULL, 10000 ); GO INSERT INTO dbo.Personnel ( EmployeeID, Name, Department, Position, ReportTo, Salary ) VALUES ( 2, 'Sara', 'Accounting', 'Accountant', 1, 5000 ); GO INSERT INTO dbo.Personnel ( EmployeeID, Name, Department, Position, ReportTo, Salary ) VALUES ( 3, 'Amy', 'Accounting', 'Accountant', 1, 3000 ); GO
创建Schema和内联函数
CREATE SCHEMA Security; GO CREATE FUNCTION Security.fn_SecurityPredicate(@Name AS SYSNAME) RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT 1 AS fn_SecurityPredicate_result WHERE @Name = USER_NAME() OR USER_NAME() = 'Martin'; GO
创建Security Policy
CREATE SECURITY POLICY SECPOL_PersonnelFilter ADD FILTER PREDICATE Security.fn_SecurityPredicate(Name,Position) ON dbo.Personnel WITH (STATE = ON);
授权给三个数据库用户访问表的权限
GRANT SELECT ON dbo.Personnel TO [Amy] GRANT SELECT ON dbo.Personnel TO [Martin] GRANT SELECT ON dbo.Personnel TO [Sara]
开始测试用户Amy能访问多少行数据
EXECUTE AS USER = 'Amy'; SELECT USER_NAME(),* FROM dbo.Personnel; REVERT;
结果
测试用户Martin
EXECUTE AS USER = 'Martin'; SELECT USER_NAME(),* FROM dbo.Personnel; REVERT;
结果
测试用户Sara
EXECUTE AS USER = 'Sara'; SELECT USER_NAME(),* FROM dbo.Personnel; REVERT;
结果
RLS确实是SQL Server 2016的一个很有用处的特性。只是它身上也有一些”缺点“。以上面这个例子来讲,如果你需要实现CEO可以知道整个公司的工资,然后部门经理可以知道整个部门的员工的工资,甚至更复杂点。那么上面那个内联函数就变得复杂了,需要JOIN一些表去判定用户的职位高低来确定他的访问权限范围。而我们都知道在内联函数中加入表引用是很容易引发性能问题的。所以其实像上面这样的办法不是一个好的方案。真正解决办法其实是通过把用户的安全上下文存为一个字符串的变量的形式传入给内联函数,内联函数内部再去利用好这个安全上下文的信息去定义好数据访问的安全控制逻辑。这点在SQL Server 2016提供了一个SESSION_CONTEXT标量函数来配合实现这一方法。这种方法才可能是今后在SQL Server 2016 RTM发布后在现实中去使用RLS的Best Practices。
参考:
ALTER SECURITY POLICY (Transact-SQL)
DROP SECURITY POLICY (Transact-SQL)
sys.security_policies (Transact-SQL)
sys.security_predicates (Transact-SQL)