如何在SQL Server中发现和处理孤立的数据库用户

介绍 (Introduction)

Context

语境

As SQL Server database administrators, we should all know that, most of the time, a database user is linked to a SQL Server login. We do this to tell SQL Server that a SQL login LoginA has access to database Db1 using the context and permissions of UserA database user.

作为SQL Server数据库管理员,我们都应该知道,大多数时候,数据库用户都链接到SQL Server登录名。 我们这样做是为了告诉SQL Server SQL登录LoginA使用UserA数据库用户的上下文和权限来访问数据库Db1 。

如何在SQL Server中发现和处理孤立的数据库用户_第1张图片

There are exceptions like the two following ones:

有如下两个例外:

  1. LoginA has very high permissions at server level like being member of sysadmin role
  2. LoginA在服务器级别具有很高的权限,例如成为sysadmin角色的成员
  3. Db1 is self-contained, which means that it uses its own authentication and does not deal with SQL Logins. (SQL Server 2012+)
  4. Db1是自包含的,这意味着它使用自己的身份验证,并且不处理SQL登录名。 (SQL Server 2012+)

We might think that this linkage is done by matching names, but it’s not the case. Normalization comes in and this mapping is actually stored using a unique identifier called “SID”:

我们可能认为这种链接是通过匹配名称完成的,但事实并非如此。 进行标准化,该映射实际上是使用称为“ SID”的唯一标识符存储的:

We can check SIDs for all our server principals using the following query :

我们可以使用以下查询来检查所有服务器主体的SID:

 
select principal_id,[sid],name 
from sys.server_principals
;
 

If we look at documentation page of the sys.server_principals view, we can see that :

如果查看sys.server_principals视图的文档页面 ,我们可以看到:

  • The principal_id is like an identity column. So, if we drop a login and recreate it, the principal_id won’t be the same.
  • Principal_id就像一个标识列。 因此,如果我们删除登录名并重新创建登录名,则principal_id将不相同。
  • The sid column defines a security identifier for the principal that will be the same for Windows authenticated logins. This means that if we use mixed authentication and we create a login then drop and create it again, it won’t have the same SID.
  • sid列定义了主体的安全标识符,该标识符与Windows身份验证的登录名相同。 这意味着,如果我们使用混合身份验证并创建一个登录名,然后删除并再次创建它,则它将没有相同的SID。

If a mapping is defined for a given SQL Login, then its corresponding SID can be found in sys.database_principals using the following query:

如果为给定SQL登录名定义了映射,则可以使用以下查询在sys.database_principals中找到其对应的SID:

 
select [sid],name 
from sys.database_principals
 

For simplicity, it’s common, but not mandatory, to use the exact same name for both SQL Login and Database User.

为简单起见,对SQL登录名和数据库用户使用完全相同的名称是常见的,但不是强制性的。

As you could expect, this linkage can be “broken” in many different ways that makes this definition “inconsistent”, that leaves database users without any link to a login. When this happens, we refer to such a database user as an orphaned database user.

如您所料,此链接可以通过许多不同的方式“断开”,从而使该定义“不一致”,从而使数据库用户无法登录。 发生这种情况时,我们将这种数据库用户称为孤立的数据库用户。

The situation is summarized in following figure.

下图总结了这种情况。

如何在SQL Server中发现和处理孤立的数据库用户_第2张图片

Let’s enlist some use cases that lead to orphaned database users:

让我们列举一些导致孤立数据库用户的用例:

  • 如何在SQL Server中发现和处理孤立的数据库用户_第3张图片

  • When using mixed authentication mode, restoring a database backup to another SQL Server instance.

    使用混合身份验证模式时,将数据库备份还原到另一个SQL Server实例。
  • After a database mirroring failover.

    数据库镜像故障转移之后。

As you can see above, there are two possible ways to manage these orphaned users:

如您在上面看到的,有两种可能的方法来管理这些孤立用户:

  1. Drop them as they are no longer needed

    丢弃它们,因为不再需要它们
  2. Link them back to the SQL Server login with the appropriate name

    将它们链接回具有适当名称SQL Server登录名

In the following sections, we will see how to list those orphaned database users and how to handle both cases listed above. Finally, we will review stored procedures that will be helpful in that situation.

在以下各节中,我们将看到如何列出那些孤立的数据库用户以及如何处理上面列出的两种情况。 最后,我们将回顾对这种情况有帮助的存储过程。

孤立数据库用户发现 (Orphaned Database User discovery)

To be able to get a list of orphaned users for every databases of a given SQL Server instance, you have to run the following statement against each of them:

为了能够获得给定SQL Server实例的每个数据库的孤立用户列表,您必须针对每个数据库运行以下语句:

 
Use [YourDbName] ;
GO
exec sp_change_users_login @Action='Report' ;
GO
 

This stored procedure will return a two-columns dataset with firstly the name of an orphaned database user and secondly its corresponding security identifier.

该存储过程将返回一个两列的数据集,其第一是孤立数据库用户的名称,第二是其相应的安全标识符。

Alternatively, we can get the list of SIDs that are defined in sys.database_principals but not in sys.server_principals as follows:

或者,我们可以获取在sys.database_principals中定义但不在sys.server_principals中定义的SID列表,如下所示:

 
select p.name,p.sid
from sys.database_principals p
where p.type in ('G','S','U')
and p.sid not in (select sid from sys.server_principals)
and p.name not in (
    'dbo',
    'guest',
    'INFORMATION_SCHEMA',
    'sys',
    'MS_DataCollectorInternalUser'
) ;
 

处理孤立的数据库用户 (Handling an orphaned database user)

As we said previously, there are two ways to handle orphaned database users: either we drop or remap them.

如前所述,有两种方法可以处理孤立的数据库用户:要么删除它们,要么重新映射它们。

Database User Drop

数据库用户删除

Dropping a database user seems pretty straight forwards: we simply need to run the DROP USER statement and it’s over. But what if this database user owns objects inside the database? It’s not that simple…

删除数据库用户似乎很简单:我们只需要运行DROP USER语句就可以了。 但是,如果此数据库用户拥有数据库内部的对象怎么办? 不是那么简单...

If it’s really the action we want to perform, we will need not only to take a look at that aspect and correct it, but also to check that this user is not used as the execution context of one or more stored procedures, functions or event notifications. Here is the error we could get if that’s the case:

如果这确实是我们要执行的操作,我们不仅需要查看并纠正该方面,还需要检查该用户是否未被用作一个或多个存储过程,函数或事件的执行上下文。通知。 如果是这种情况,这是我们可能得到的错误:

Msg 15136, Level 16, State 1

消息15136,第16级,状态1

We will also need to remove group membership of that user and there are maybe some other aspects that I don’t cover it because I’ve never been confronted to them.

我们还需要删除该用户的组成员身份,也许还有其他一些我不涉及的方面,因为我从未遇到过他们。

An analysis of these aspects can be quite long and has to be done for all orphaned users. Plus, this action can be used in other database management tasks. So, this is a good candidate for automation and you will find attached to this article a script with a stored procedure that takes care of dropping a database user and performs some pre-checks before that. The script is called

对这些方面的分析可能相当长,必须对所有孤立用户进行分析。 另外,此操作可以在其他数据库管理任务中使用。 因此,这是自动化的一个很好的候选者,并且您会在本文中找到带有存储过程的脚本,该脚本负责删除数据库用户并在此之前执行一些预检查。 该脚本称为

Let’s review the details of this stored procedure.

让我们查看此存储过程的详细信息。

First, let’s see its parameters

首先,让我们看看它的参数

 
ALTER PROCEDURE [Administration].[DropDatabaseUser] (
    @DbName                         VARCHAR(256),
    @UserName                       VARCHAR(256),
    @ResetOwnership                 BIT             = 1, 
    @NewObjectOwner                 VARCHAR(256)    = 'dbo',
    @Debug                          BIT             = 0
)
 

As you can see, it takes the name of the database in which we want to perform the drop and the name of the database user we want to drop. There is a flag bit to tell the procedure to assign database objects owned by @UserName to another database user @NewObjectOwner. By default, this user is dbo. Finally, there is a flag bit that will enable a debug mode and make this procedure more talkative.

如您所见,它采用了我们要在其中执行删除操作的数据库的名称以及我们要删除的数据库用户的名称。 有一个标志位告诉该过程将@UserName拥有的数据库对象分配给另一个数据库用户@NewObjectOwner 。 默认情况下,该用户是dbo 。 最后,有一个标志位将启用调试模式并使此过程更加健谈。

The first part of the stored procedure is a set of pre-requisities checks :

存储过程的第一部分是一组先需求检查:

  • Check that database exists

    检查数据库是否存在
  • Check that database user exists

    检查数据库用户是否存在
  • Check that the object owner for @UserName’s object exists in the database, only if @ResetOwnership parameter is set to 1.
  • 仅当@ResetOwnership参数设置为1时,检查数据库中是否存在@UserName对象的对象所有者。
  • Check that the database user we want to drop is not a MS-Shipped user like dbo or INFORMATION_SCHEMA.
  • 检查我们要删除的数据库用户是否不是dbo或INFORMATION_SCHEMA之类的MS-Shipped用户。
  • Check modules with an EXECUTE AS statement that refers to the database user we want to drop. If any module is found, it’s returned by a SELECT statement.
  • 使用EXECUTE AS语句检查模块,该语句引用我们要删除的数据库用户。 如果找到任何模块,则由SELECT语句返回。

Then it will get the list of owned schemas and owned roles and raise an error whenever @ResetOwnership parameter is not set to 1 and it founds something.

然后,只要@ResetOwnership参数未设置为1且找到了内容,它将获取拥有的架构和拥有的角色的列表,并引发错误。

Finally it will stack roles of which @UserName is member.

最后,它将堆叠@UserName成员的角色。

Once the list is complete, it will loop and either :

列表完成后,它将循环播放,并且:

  • Put an end to role membership using sp_droprolemember stored procedure
  • 使用sp_droprolemember存储过程终止角色成员资格
  • Transfer ownership of schema or role using an ALTER AUTHORIZATION statement
  • 使用ALTER AUTHORIZATION语句转移模式或角色的所有权

When all that is done, we can finally run the DROP USER statement.

完成所有这些操作后,我们最终可以运行DROP USER语句。

Database User Remapping

数据库用户重新映射

For remapping, we must use the sp_change_users_login stored procedure we already used to report orphaned users.

为了进行重新映射,我们必须使用已经用于报告孤立用户的sp_change_users_login存储过程。

We can run it for a single database user mapping as follows:

我们可以为单个数据库用户映射运行它,如下所示:

 
EXEC dbo.sp_change_users_login 
                            @Action          = 'update_one', 
                            @UserNamePattern = 'UserA', 
                            @LoginName       = 'LoginA' 
;
 

Alternatively, we can try the autofix parameter :

另外,我们可以尝试使用autofix参数:

 
EXEC dbo.sp_change_users_login 'auto_fix', 'UserA';
 

But running this requires that the name of SQL Login to be the exact same as the database user we want to remap.

但是运行此命令要求SQL登录名与我们要重新映射的数据库用户完全相同。

This procedure also allows to create the login when it doesn’t exist.

此过程还允许创建不存在的登录名。

 
EXEC dbo.sp_change_users_login 'auto_fix', 'UserA, 
                               null,'aVeryStrongP@ssw0rd' ;
 

In any of the methods presented above, this has to be performed one database at a time.

在上面介绍的任何方法中,都必须一次在一个数据库中执行此操作。

孤儿的实例级管理 (Instance-level management of orphaned users)

Components of the solution

解决方案的组成部分

In the previous section, we saw how to manage orphaned users for one database at a time. While this is good for testing purpose, we might think that it’s not affordable for production environments where we sometimes have 10 or more databases that have been restored and that we must take care of.

在上一节中,我们了解了如何一次管理一个数据库的孤立用户。 尽管这对于测试目的是有好处的,但我们可能会认为对于生产环境而言,这是负担不起的,因为生产环境中有时已经还原了10个或更多数据库,因此我们必须加以照顾。

That’s the reason why I developed a stored procedure called Administration.FindOrphanUser that will loop across all databases of the instance and optionally try to fix orphaned users (which means remapping) and/or drop them.

这就是为什么我开发了一个称为Administration.FindOrphanUser的存储过程的原因,该过程将遍历实例的所有数据库,并有选择地尝试修复孤立的用户(这意味着重新映射)和/或删除它们。

It’s built on a stored procedure called Common.RunQueryAcrossDatabases that runs a T-SQL statement across a set of databases. It’s like the undocumented sp_MSforeachdb stored procedure, but with its differences.

它基于称为Common.RunQueryAcrossDatabases的存储过程建立,该存储过程跨一组数据​​库运行T-SQL语句。 就像未记录的sp_MSforeachdb存储过程一样,但是有所不同。

Both Administration.FindOrphanUser and Common.RunQueryAcrossDatabases procedures are attached to this article.

本文附带了Administration.FindOrphanUser和Common.RunQueryAcrossDatabases过程。

As it’s not the aim of this article, I won’t insist on Common.RunQueryAcrossDatabases procedure and we will focus on the other one.

由于这不是本文的目的,因此我不会坚持使用Common.RunQueryAcrossDatabases过程,而我们将专注于另一过程。

So, what’s its interface? It takes 5 parameters:

那么,它的界面是什么? 它包含5个参数:

  • an optional database name

    可选的数据库名称
  • a flag bit to tell whether or not try to remap database users to a login of the same name

    一个标志位,告诉您是否尝试将数据库用户重新映射到同名登录名
  • a flag bit to tell whether or not try to drop remaining orphaned database users (after the trial to remap has been done)

    一个标志位,用于指示是否尝试删除剩余的孤立数据库用户(在尝试重新映射之后)
  • a parameter to tell whether to get the list of orphaned users or an execution report. Two possible values then: ‘REPORT’ or ‘TABLE’. All other values than these ones are equivalent to a ‘NONE’ output value and no dataset will be returned by the stored procedure.
  • 一个参数,告诉您要获取孤立用户列表还是执行报告。 然后有两个可能的值: 'REPORT'或'TABLE' 。 除这些值外的所有其他值都等于“ NONE”输出值,并且存储过程将不返回任何数据集。

This gives the following in T-SQL:

这在T-SQL中提供了以下内容:

 
ALTER PROCEDURE [Administration].[FindOrphanUsers] (
    @DatabaseName                   VARCHAR(256)    = NULL,
    @TryToFix                       BIT             = 0,
    @DropUnfixable                  BIT             = 0,
    @OutputType                     VARCHAR(16)     = 'TABLE',  
    @Debug                          BIT             = 0
)
 

This stored procedure will create a temporary table #OrphanUsersData that will contain all the information (and a little more) that are manipulated during execution.

此存储过程将创建一个临时表#OrphanUsersData ,其中将包含在执行过程中操纵的所有信息(以及更多信息)。

Here is the creation statement for this table.

这是此表的创建语句。

 
CREATE TABLE #OrphanUsersData (
        DbName              VARCHAR(256),
        UserName            VARCHAR(256),
        UserType            VARCHAR(256),
        DefaultSchemaName   VARCHAR(256),
        CreationDate        DATE,
        ModifiedDate        DATE,
        FixIssue            VARCHAR(16),
        DropIssue           VARCHAR(16),
        DDL2Drop            VARCHAR(MAX),
        DDL2Remap           AS  'Use ' + QUOTENAME(DbName) + ';' + CHAR(13) + CHAR(10) +
                                'EXEC dbo.sp_change_users_login @Action = ''update_one'', @UserNamePattern = ''' + UserName + ''', @LoginName = ''' + UserName + ''';' + CHAR(13) + CHAR(10)
    );
 

Let’s review each column.

让我们回顾一下每一列。

We have obviously columns for database name and user name. Then we have some general information on that user:

我们显然有数据库名称和用户名的列。 然后,我们获得了有关该用户的一些常规信息:

  • The UserType column should be either SQL_USER or WINDOWS_USER and corresponds to the type_desc column in sys.database_principals.
  • UserType列应为SQL_USER或WINDOWS_USER,并与sys.database_principals中的type_desc列相对应。
  • The name of the default schema. It’s true we didn’t mention it previously, but each database user has a default schema (which is, in my experience and unfortunately most of times dbo).
  • 默认架构的名称。 的确,我们之前没有提到它,但是每个数据库用户都有一个默认模式(根据我的经验,不幸的是,大多数情况下是dbo )。
  • Its creation and last modification dates

    它的创建和最后修改日期

The next two columns are related to the execution of Administration.FindOrphanUser stored procedure. It will contain the outcome of the execution that is implied by @TryToFix and @DropUnfixable parameters.

接下来的两列与Administration.FindOrphanUser存储过程的执行有关。 它包含@TryToFix和@DropUnfixable参数所隐含的执行结果。

Finally, we have the last two columns that will contain DDL statements. The first one will be used to drop the database user (it will actually be a call to former [Administration].[DropDatabaseUser] stored procedure). The second one will contain the statement for database user to login re-mapping. As this statement is pretty simple, we created it as a computed column.

最后,我们有最后两列将包含DDL语句。 第一个将用于删除数据库用户(实际上是对先前的[Administration]。[DropDatabaseUser]存储过程的调用)。 第二个语句将包含供数据库用户登录重新映射的语句。 由于该语句非常简单,因此我们将其创建为计算列。

This table will be used as destination in the query we will provide to Common.RunQueryAcrossDatabases stored procedure. Actually, this query will be formed as follows:

该表将在我们提供给Common.RunQueryAcrossDatabases存储过程的查询中用作目标。 实际上,此查询将形成如下:

    
INSERT INTO #OrphanUsersData