如何防止用户(有 db_datawriter)能够通过管理工作室连接?
How to prevent user (having db_datawriter) from being able to connect via management studio?
我有一个 windows sql 用户在数据库上分配了 db_datawriter 角色。
目前用户可以通过management studio连接到数据库并对数据库中的数据进行更改。
此外,他还可以使用 windows 表单应用程序,使用他的 windows 凭据连接到数据库(使用连接字符串)并修改数据。
我想阻止该用户通过 Management Studio 进行连接。但继续允许他通过 windows 表单应用程序修改数据。
这部分是一个安全问题,但也是一个设计问题,所以我会从两个方面给你一个答案。
使用接口
db_datawriter
几乎总是过于宽容。应用程序实际上很少需要以临时方式写入每个 table 的权限。打个比方,想想你在构建应用程序时创建的 classes。他们看起来像这样吗?
class MyClass
{
public int someIntIneedForAnInternalAlgorithm;
public string someStringThatCanOnlyChangeInternally;
public ISomeInterface SomeInterfaceMyImplementationNeeds;
// ...
}
可能不会。他们应该看起来更像这样:
class MyClass
{
public MyClass(ISomeInterface injectedInterface) { // ...
private int someIntIneedForAnInternalAlgorithm;
public string SomeStringThatCanOnlyChangeInternally { get; private set; }
private readonly ISomeInterface theInterfaceIneeded;
// ...
换句话说,class 的某些内部状态通常是私有的。您可以通过一些受控的 public 界面与它交互。
一个数据库也不例外。您可以将 table 视为“内部状态”。应用程序不需要直接访问所有这些,它应该通过接口访问它。界面可以由视图、函数或存储过程组成。然后,根据 principle of least privilege
,您仅授予对“public 接口”中对象的访问权限
您可能仍希望允许用户任意读取 tables - 例如,您可能足够信任他们让他们编写自己的查询以用于报告目的。但是你不能让他们任意写入 tables.
这不是一个完整的解决方案:如果您的用户能够使用他们自己的凭据进行连接,他们仍然可以直接通过该界面操作数据库。但至少他们没有临时权限与 table 任意交互。
应用程序凭据
如果这还不够怎么办?如果您想确保与数据库交互的唯一方式是通过应用程序怎么办?好吧,那么您为应用程序 创建一个登录名 。应用程序代表用户自行连接,而不是转发用户的凭据。
当然,这种方法也有缺点。一方面,这意味着您丢失了有关谁在做什么的信息。以前,如果您在 table 上有类似 modified_by
的列,您可以使用 original_login()
函数之类的东西充满信心地填充该列。但是现在 original_login()
函数将 return 应用程序登录。因此,您必须以其他方式提供用户信息,例如作为存储过程的参数数据,或者将其作为 session context.
传递
另一个缺点是您可能一直在使用活动目录安全组来提供更精细的权限。例如,您可能已经为 MyCompany\Sales Department
创建了一个登录名,并授予该组特殊权限以便能够与销售数据进行交互。但是现在应用程序正在为每个人做登录,所以它需要做所有事情的能力。
我的蛋糕可以一起吃吗?
“好的”,我听到你说,“我理解这些想法,但事情是这样的。我想根据应用程序的身份控制访问,但我也想根据用户的身份控制访问。”。 =27=]
乍一看,这个问题的答案似乎是“倒霉”吧?怎么样,这不是自相矛盾吗?您要么以用户身份登录,要么以应用程序身份登录。不可能两者兼而有之。所以你不能根据两者来控制权限。
但是这个答案确实做了一个假设:它假设获取身份信息的唯一方法是通过传输凭证(即登录)。但这并不完全正确。有两种方法可以避免这种明显的矛盾:
- 使用应用程序身份登录,将用户身份作为数据传递,并在数据库内部使用用户身份数据解析附加权限,或者...
- 使用用户的凭据登录,使用正常的 SQL 服务器角色和权限来提供对界面的访问,但要确保登录来自应用程序。
你是怎么做到这些的?哪个更容易实施?哪个更安全?
很遗憾,第二个问题的答案与第三个问题的答案不同。
我们先看第一种方式。假设您在访问数据库时将用户的活动目录用户名传递给数据库。您可以检查该用户的安全组吗?当然。您可以使用 xp_logininfo
过程来获取某些给定用户名的“权限路径”。但我什至不打算开始详细介绍如何编写数据库接口才能使用它。在涉及使用 execute as
时,它涉及通过数据手动解析 sql 权限...这只是大量工作的地狱。
那么,算了,第二种方式呢?好吧,那要容易得多。你想出了一些秘密钥匙。您将密钥提供给应用程序。该应用程序仍会转发用户凭据以进行登录,但是当有人尝试使用您的数据库接口中的某个对象时,您会检查密钥是否正确。您可以为此再次使用 session_context
。
这是不是“有点乱”?可能是。但是,嘿,我们正在尝试同时以两种不同的方式控制权限。绝对的优雅不是我们能达到的。
在存储过程中使用此方法
create or alter procedure p as begin
set nocount on;
if (isnull(session_context(N'applicationKey'), '') != 'my secret value')
throw 50001, 'Access to this database is only allowed through MyApplication', 1;
/*
do work here
*/
end
在视图中使用它...
create view v as
select *
from sys.objects
where session_context(N'applicationKey') = 'my secret value'
快速、肮脏、潜在危险的 hack 如果你想要最少的代码更改
还有另一种方法,但它有很多问题,我不会真正推荐它:登录触发器。要使用它,您需要将应用程序名称添加到您的连接字符串中,并确保您也设置了一个初始目录
"Data Source=MyServer;Initial Catalog=myDatabase;Application Name=myappname; Integrated Security=SSPI"
然后检查登录触发器中连接字符串的应用程序名称值。您还想确保您不阻止其他可能有效的登录连接,因此也要检查数据库名称,并检查登录的用户是否是您要限制的组的成员。在此示例中,我使用了 db_datareader
角色,但我建议创建一个特定于您的应用程序的角色:
CREATE TRIGGER application_check
ON ALL SERVER WITH EXECUTE AS 'sa'
for logon
AS
begin
if (
is_member(original_login(), 'db_datareader')
and db_name() = 'mydatabase'
and app_name() != 'myappname'
) rollback;
end
我有一个 windows sql 用户在数据库上分配了 db_datawriter 角色。
目前用户可以通过management studio连接到数据库并对数据库中的数据进行更改。
此外,他还可以使用 windows 表单应用程序,使用他的 windows 凭据连接到数据库(使用连接字符串)并修改数据。
我想阻止该用户通过 Management Studio 进行连接。但继续允许他通过 windows 表单应用程序修改数据。
这部分是一个安全问题,但也是一个设计问题,所以我会从两个方面给你一个答案。
使用接口
db_datawriter
几乎总是过于宽容。应用程序实际上很少需要以临时方式写入每个 table 的权限。打个比方,想想你在构建应用程序时创建的 classes。他们看起来像这样吗?
class MyClass
{
public int someIntIneedForAnInternalAlgorithm;
public string someStringThatCanOnlyChangeInternally;
public ISomeInterface SomeInterfaceMyImplementationNeeds;
// ...
}
可能不会。他们应该看起来更像这样:
class MyClass
{
public MyClass(ISomeInterface injectedInterface) { // ...
private int someIntIneedForAnInternalAlgorithm;
public string SomeStringThatCanOnlyChangeInternally { get; private set; }
private readonly ISomeInterface theInterfaceIneeded;
// ...
换句话说,class 的某些内部状态通常是私有的。您可以通过一些受控的 public 界面与它交互。
一个数据库也不例外。您可以将 table 视为“内部状态”。应用程序不需要直接访问所有这些,它应该通过接口访问它。界面可以由视图、函数或存储过程组成。然后,根据 principle of least privilege
,您仅授予对“public 接口”中对象的访问权限您可能仍希望允许用户任意读取 tables - 例如,您可能足够信任他们让他们编写自己的查询以用于报告目的。但是你不能让他们任意写入 tables.
这不是一个完整的解决方案:如果您的用户能够使用他们自己的凭据进行连接,他们仍然可以直接通过该界面操作数据库。但至少他们没有临时权限与 table 任意交互。
应用程序凭据
如果这还不够怎么办?如果您想确保与数据库交互的唯一方式是通过应用程序怎么办?好吧,那么您为应用程序 创建一个登录名 。应用程序代表用户自行连接,而不是转发用户的凭据。
当然,这种方法也有缺点。一方面,这意味着您丢失了有关谁在做什么的信息。以前,如果您在 table 上有类似 modified_by
的列,您可以使用 original_login()
函数之类的东西充满信心地填充该列。但是现在 original_login()
函数将 return 应用程序登录。因此,您必须以其他方式提供用户信息,例如作为存储过程的参数数据,或者将其作为 session context.
另一个缺点是您可能一直在使用活动目录安全组来提供更精细的权限。例如,您可能已经为 MyCompany\Sales Department
创建了一个登录名,并授予该组特殊权限以便能够与销售数据进行交互。但是现在应用程序正在为每个人做登录,所以它需要做所有事情的能力。
我的蛋糕可以一起吃吗?
“好的”,我听到你说,“我理解这些想法,但事情是这样的。我想根据应用程序的身份控制访问,但我也想根据用户的身份控制访问。”。 =27=]
乍一看,这个问题的答案似乎是“倒霉”吧?怎么样,这不是自相矛盾吗?您要么以用户身份登录,要么以应用程序身份登录。不可能两者兼而有之。所以你不能根据两者来控制权限。
但是这个答案确实做了一个假设:它假设获取身份信息的唯一方法是通过传输凭证(即登录)。但这并不完全正确。有两种方法可以避免这种明显的矛盾:
- 使用应用程序身份登录,将用户身份作为数据传递,并在数据库内部使用用户身份数据解析附加权限,或者...
- 使用用户的凭据登录,使用正常的 SQL 服务器角色和权限来提供对界面的访问,但要确保登录来自应用程序。
你是怎么做到这些的?哪个更容易实施?哪个更安全?
很遗憾,第二个问题的答案与第三个问题的答案不同。
我们先看第一种方式。假设您在访问数据库时将用户的活动目录用户名传递给数据库。您可以检查该用户的安全组吗?当然。您可以使用 xp_logininfo
过程来获取某些给定用户名的“权限路径”。但我什至不打算开始详细介绍如何编写数据库接口才能使用它。在涉及使用 execute as
时,它涉及通过数据手动解析 sql 权限...这只是大量工作的地狱。
那么,算了,第二种方式呢?好吧,那要容易得多。你想出了一些秘密钥匙。您将密钥提供给应用程序。该应用程序仍会转发用户凭据以进行登录,但是当有人尝试使用您的数据库接口中的某个对象时,您会检查密钥是否正确。您可以为此再次使用 session_context
。
这是不是“有点乱”?可能是。但是,嘿,我们正在尝试同时以两种不同的方式控制权限。绝对的优雅不是我们能达到的。
在存储过程中使用此方法
create or alter procedure p as begin
set nocount on;
if (isnull(session_context(N'applicationKey'), '') != 'my secret value')
throw 50001, 'Access to this database is only allowed through MyApplication', 1;
/*
do work here
*/
end
在视图中使用它...
create view v as
select *
from sys.objects
where session_context(N'applicationKey') = 'my secret value'
快速、肮脏、潜在危险的 hack 如果你想要最少的代码更改
还有另一种方法,但它有很多问题,我不会真正推荐它:登录触发器。要使用它,您需要将应用程序名称添加到您的连接字符串中,并确保您也设置了一个初始目录
"Data Source=MyServer;Initial Catalog=myDatabase;Application Name=myappname; Integrated Security=SSPI"
然后检查登录触发器中连接字符串的应用程序名称值。您还想确保您不阻止其他可能有效的登录连接,因此也要检查数据库名称,并检查登录的用户是否是您要限制的组的成员。在此示例中,我使用了 db_datareader
角色,但我建议创建一个特定于您的应用程序的角色:
CREATE TRIGGER application_check
ON ALL SERVER WITH EXECUTE AS 'sa'
for logon
AS
begin
if (
is_member(original_login(), 'db_datareader')
and db_name() = 'mydatabase'
and app_name() != 'myappname'
) rollback;
end