审校 | 重楼
概要
现代的企业数据库具有全面的安全机制,可以对图表和视图中的数据实施细粒度访问控制。
然而,当涉及到存储过程时,访问控制机制可能相对粗略:能够执行存储过程,或者不能。
本文将展示一种使用可编程代理的更灵活的方法,该方法允许基于所有可用信息(例如参数值、返回值等)精确控制存储过程的调用。对于那些在管理对企业数据库的访问时需要更多粒度和灵活性的人来说,这种方法可能会引起他们的兴趣。
细粒度访问控制
大多数企业数据库都提供了细粒度的安全机制,确保只有经过授权的用户才能对数据进行访问和修改,而且可以控制到具体的行和列级别。例如:
- Microsoft SQL Server具有行级安全性和列级授权
- IBM DB2具有行和列访问控制以及相当精细的基于标签的访问控制
- Oracle具有细粒度访问控制、Oracle标签安全性和其他一些机制
在所有情况下,概念都是相同的:应该能够在非常细粒度的级别上指定哪些操作可以由哪些用户对哪些数据执行。
但是,当涉及到存储过程时,所有数据库都有一个简单的访问/不访问机制。用户可以执行给定的存储过程,也可以不执行。
是否需要对存储过程进行细粒度访问控制?
许多组织大量使用存储过程,有时甚至会完全阻止对图表和视图的直接访问,所有数据访问都必须通过存储过程。
存储过程本身通常必须提供复杂的访问控制作为其实现的一部分,以确保调用有效,即使底层数据本身受到细粒度访问控制的保护。这使得存储过程更加复杂,调用成本更高,更改也更频繁。
对许多人来说,这只是开展业务的成本,但在某些情况下,基于代理的方法在以下情况下很有用:
- 限制取决于难以从SQL访问的外部数据
- 存储过程不容易修改以反映安全需求,例如,因为它们是用户无法控制的第三方包的一部分
- 用户没有访问数据库的特权
- 作为架构师,更倾向于用精细化访问控制外部化,而不是将其嵌入到存储过程中
- 需要更改一些参数值或将调用重定向到不同的存储过程
即使这些方法都不适合,也可以寻求其他方法以开辟新的道路。
可以使用代理控制什么?
可编程数据库代理可以控制数据库服务器和客户机之间的任何事项,但本文将重点讨论存储过程。
通过引入可编程代理,可以控制存储过程调用的三个关键方面:
- 调用本身
- 传递给存储过程的参数值
- 存储过程返回的值或结果集
(1)控制调用
代理可以拒绝或修改存储过程的调用。这可能有以下几个原因:
- 参数值不可接受
- 场景不正确:调用来自意外的地址,代理检测到意外的行为模式或任何其他因素
- 可以将调用重定向到不同的存储过程,并且可以相应地调整参数
- 调用可以调用多个存储过程,并合并结果
(2)控制参数值
代理还可以对客户端传递的参数执行逻辑:
- 可记录或记录参数值
- 逻辑可以验证参数是否具有可接受的值
- 逻辑可以修改这些参数的值
- 逻辑可以拒绝参数
(3)控制返回值和结果集
一旦执行了存储过程,它可能会返回一些数据,这些数据可能是单独的值,也可能是一个或多个结果集。
基于这些值或结果集,代理可以:
- 让一切都流向客户端
- 停止调用并向客户端返回错误,例如,如果代理确定客户端没有被授权查看特定的数据块
- 通过修改、隐藏、从结果集中删除行和列值等方式修改值或结果集…
- 如果确定客户端不应该有访问权限,但也不应该知道它没有访问权限,则返回null值或空结果集
如何向数据库添加代理?
向数据库添加代理通常只需启动一个或多个代理,并将客户端引导到代理即可。因此,与通常的连接不同:
在中间添加代理,并开始在代理中添加需要的任何逻辑:
让我们看一个简单的例子。将使用SQL Server作为数据库,使用Gallium Data作为代理。
给定一个简单的存储过程:
SQL
CREATE PROCEDURE DEMO.CREATE_PRODUCT (
IN NAME VARCHAR(50),
IN PRICE DECIMAL(10,2),
IN TYPEID INT)
希望实现以下要求:
(1)只有MGMT组中的用户可以创建type为98或99的产品
(2)只有type > 100的产品价格才能超过5000美元
(3)type16和type17的产品实际上必须使用CREATE_SPECIAL_PRODUCT过程创建
使用代理中的RPC筛选器很容易满足第一个要求:
JavaScript
let typeId = context.packet.parameters[2].value;
if (typeId === 98 || typeId === 99) {
let rs = context.mssqlutils.executeQuery("select is_member('MGMT') as res");
let isMember = rs.rows[0].res;
if ( ! isMember) {
context.result.errorMessage = "User is not a member of the MGMT group";
return;
}
}
如果需求没有得到满足,这将导致客户端接收到错误提示。
第二个示例要求是一个简单的扩展:
JavaScript
let price = context.packet.parameters[1].value;
if (price > 5000 && typeId <= 100) {
context.result.errorMessage = "Price is too high for this type of product";
return;
}
第三个示例要求更简单:
JavaScript
if (typeId === 16 || typeId === 17) {
context.packet.procName = "CREATE_SPECIAL_PRODUCT";
}
在最后一个示例中,假设CREATE_SPECIAL_PRODUCT过程采用与CREATE_PRODUCT相同的参数,但是如果情况并非如此,其逻辑当然可以根据需要更改参数。
这些都是简单的示例,但可以让人们了解:使用可编程数据库代理保护存储过程相当简单,如果不能(或不想)更改存储过程,那么它尤其有用。
原文Fine-Grained Access Control for Stored Procedures,作者:Max Tardiveau