如何使用MongoDB访问控制
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
现代数据库系统能够存储和处理大量数据。 因此,很少有任何一个用户单独负责处理与管理数据库相关的所有活动。 通常,不同的数据库用户需要对数据库的某些部分具有不同级别的访问权限:一些用户可能只需要读取特定数据库中的数据,而其他用户必须能够插入新文档或修改现有文档。 同样,应用程序可能需要唯一权限,仅允许它访问它需要运行的数据库部分。
MongoDB 采用一种健壮的机制来控制对称为 基于角色的访问控制 (RBAC) 的数据库系统的访问和特权。 在本教程中,您将了解 RBAC 的工作原理、最小权限原则的含义和目的,以及如何在实践中使用 MongoDB 的访问权限特性。
先决条件
要遵循本教程,您将需要:
- 具有
sudo
权限的常规非 root 用户和配置了 UFW 的防火墙的服务器。 您可以按照初始服务器设置教程来准备您的服务器。 - MongoDB 安装在您的服务器上。 要进行此设置,请按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作。
- 通过启用身份验证和创建管理用户来保护服务器的 MongoDB 实例。 要像这样保护 MongoDB,请按照我们关于 如何在 Ubuntu 20.04 上保护 MongoDB 的教程进行操作。
注意: 关于如何配置服务器、安装和安全 MongoDB 安装的示例教程参考 Ubuntu 20.04。 本教程专注于 MongoDB 本身,而不是底层操作系统。 只要启用了身份验证,它通常适用于任何 MongoDB 安装,无论操作系统如何。
MongoDB 如何使用基于角色的访问控制来控制访问
访问控制——也称为授权——是一种涉及确定谁可以访问哪些资源的安全技术。
为了更好地理解 MongoDB 中的访问控制,首先将其与一个不同但密切相关的概念区分开来会有所帮助:身份验证。 Authentication 是确认用户或客户端是否真的是他们声称的身份的过程。 另一方面,Authorization 涉及为给定用户或用户组设置规则,以定义他们可以执行哪些操作以及他们可以访问哪些资源。
以下小节将详细介绍 MongoDB 如何处理身份验证和授权。
MongoDB 中的身份验证
在许多数据库管理系统中,仅通过用户名和密码对来识别用户。 使用有效凭据连接到数据库时,用户将通过身份验证并授予与该用户关联的访问级别。 在这种方法中,用户目录是平面的,这意味着对于整个数据库服务器,每个用户名都必须是唯一的。
相比之下,MongoDB 采用了更复杂的用户目录结构。 在 MongoDB 中,用户不仅由他们的用户名标识,而且由创建他们的 数据库 标识。 对于每个用户,创建他们的数据库称为该用户的 身份验证数据库 。
这意味着在 MongoDB 中,只要在不同的身份验证数据库中创建,就可以有多个用户具有相同的用户名(例如 sammy)。 要以用户身份进行身份验证,您不仅必须提供用户名和密码,还必须提供与该用户关联的身份验证数据库的名称。
有人可能会假设在给定身份验证数据库中创建的用户将具有仅对该特定数据库可用的访问权限,但事实并非如此。 每个用户,无论它是在哪个身份验证数据库中创建的,都可以在不同的数据库中分配权限。
MongoDB 中的授权(基于角色的访问控制)
在 MongoDB 中,您可以通过称为基于角色的访问控制(通常简称为 RBAC)的机制来控制谁可以访问数据库上的哪些资源以及访问程度。
在基于角色的访问控制中,用户没有被授予直接对资源执行操作的权限,例如将新文档插入数据库或查询特定集合。 这将使安全策略难以管理并与系统中的许多用户保持一致。 相反,允许对特定资源执行操作的规则分配给 roles。
将角色视为给定用户的工作或职责之一会很有帮助。 例如,经理对公司 MongoDB 实例中的每个文档具有读写访问权限可能是有意义的,而销售分析师可能仅对销售记录具有只读访问权限。
角色由一组一个或多个 特权 定义。 每个权限都包含一个操作(例如创建新文档、从文档中检索数据或创建和删除用户)和可以执行该操作的资源(例如名为 reports
的数据库或集合称为 orders
)。 就像在现实生活中,一家公司可能有许多销售分析师和员工承担多个职责,在 MongoDB 中,可以将许多用户分配给同一个角色,而单个用户可以授予多个角色。
角色由角色名称和数据库的组合来标识,因为每个角色——除了在 admin
数据库中创建的角色——只能包括应用于其自己的数据库的权限。 通过授予用户在其身份验证数据库以外的数据库中定义的角色,可以授予用户对多个数据库执行操作的权限。 可以在您创建用户时或之后的任何时间授予角色。 撤销角色成员资格也可以随意进行,从而可以直接将用户管理与访问权限管理分离。
MongoDB提供了一组描述数据库系统常用权限的内置角色,例如read
授予只读访问权限,readWrite
授予读写权限,或dbOwner
授予对给定数据库的完全管理权限。 对于更具体的场景,还可以使用自定义权限集创建用户定义的角色。
注意:您可以在MongoDB官方文档的Built-in roles页面找到MongoDB提供的所有内置角色的详细信息。
基于角色的访问控制可以只为用户分配他们在各自任务中工作所需的最低、精确的访问权限级别。 这是一种重要的安全实践,被称为 最小权限原则 。
通过遵循本指南,您将构建一个示例数据库环境,创建一些示例数据库和用户,每个都授予不同级别的访问权限,展示基于角色的访问控制。
第 1 步 — 概述示例场景并准备示例数据库
为了解释基于角色的访问控制(简称 RBAC)如何在实践中发挥作用,本指南遵循一个示例场景,其中一个名为 Sammy Sales 的假想销售公司使用两个数据库。
第一个数据库(称为 sales
)将存储有关公司商店中客户订单的数据,其中包含两个单独的集合:customers
用于存储客户的个人数据,orders
用于存储订单详细信息。
第二个数据库(这个称为 reports
)将存储每月销售的汇总报告。 该数据库将包含一个名为 reports
的集合。
该公司只有两名员工,他们都具有遵循最小权限方法的数据库访问级别:
- 销售代表 Sammy 需要完全访问
sales
数据库中的两个集合,但不需要使用reports
数据库。 - 销售分析师 Joe 需要对
reports
数据库的写访问权来构建报告,并需要对sales
数据库的只读访问权来检索数据。
要求如下图所示:
要创建这些示例数据库,请使用如下命令打开安装了 MongoDB 的服务器上的 MongoDB shell,确保在此过程中以管理用户身份进行身份验证。 此示例遵循先决条件 如何在 Ubuntu 20.04 上保护 MongoDB 教程中建立的约定,其中管理 MongoDB 用户名为 AdminSammy。 如果不同,请务必将 AdminSammy 替换为您自己的管理用户的用户名:
mongo -u AdminSammy -p --authenticationDatabase admin
出现提示时,输入您在安装期间设置的密码以访问 shell。
您可以通过发出 show dbs
命令来验证您是否可以访问整个 MongoDB 实例:
show dbs
这将返回当前可用的所有数据库的列表:
Outputadmin 0.000GB config 0.000GB local 0.000GB
确认可以访问这些数据库后,切换到sales
数据库:
use sales
shell 将回复一个简短的确认
Outputswitched to db sales
在 MongoDB 中,没有明确的操作来创建数据库。 只有在存储至少一个文档时才会创建数据库。 考虑到这一点,您需要插入一些示例文档来准备本指南中示例中使用的数据库和集合。
您现在位于 sales
数据库中,但在您向其中插入内容之前,它实际上并不存在。
在 sales
中创建一个名为 customers
的集合,并同时通过以下操作向其中插入一个新文档:
db.customers.insert({name: 'Sammy'})
此示例文档仅包含一个值为 'Sammy'
的 name
字段。 请注意,数据本身与展示访问权限在实践中的工作方式无关,因此此步骤概述了如何创建仅包含示例模拟数据的数据库文档。
MongoDB 将通过以下方式确认插入:
OutputWriteResult({ "nInserted" : 1 })
重复此过程,但这次创建一个名为 orders
的集合。 为此,请运行以下命令。 这一次,文档的唯一字段是 total
,它的值为 100
:
db.orders.insert({total: 100})
MongoDB 将再次确认文档已正确插入。
由于您将使用两个数据库,因此您还需要准备 reports
数据库。 为此,首先切换到 reports
数据库:
use reports
并插入另一个文档,这次插入 reports
集合:
db.reports.insert({orders: 1})
要确认两个数据库都已正确准备,请再次发出 show dbs
命令。
show dbs
第二次运行此命令后,结果将显示新创建的数据库的两个新条目。 这些数据库仅在您在每个数据库中创建第一个文档后才会保留:
Outputadmin 0.000GB config 0.000GB local 0.000GB reports 0.000GB sales 0.000GB
示例数据库现已准备就绪。 现在,您可以创建一对对本示例场景所需的新创建数据库拥有最少访问权限的用户。
第 2 步 — 创建第一个用户
在此步骤中,您将创建两个 MongoDB 用户中的第一个。 第一位用户将是公司的销售代表 Sammy。 此帐户需要对 sales
数据库的完全访问权限,但对 reports
数据库没有任何访问权限。
为此,我们将使用内置的 readWrite
角色来授予对 sales
数据库中资源的读写访问权限。 由于 Sammy 是销售代表,我们还将使用 sales
数据库作为新创建用户的身份验证数据库。
首先,切换到sales
数据库:
use sales
shell 将返回确认您正在使用所选数据库:
Outputswitched to db sales
因为 Sammy 在销售部门工作,所以他们的 MongoDB 用户帐户将使用 sales
作为身份验证数据库创建。
运行以下方法创建 sammy 用户:
db.createUser( { user: "sammy", pwd: passwordPrompt(), roles: [ { role: "readWrite", db: "sales" } ] } )
这个 createUser
方法包括以下对象:
user
代表用户名,在本例中为 sammy。pwd
代表密码。 通过使用passwordPrompt()
,您将确保 MongoDB shell 在执行要输入的命令时要求输入密码。roles
是授予的角色列表。 此示例为 sammy 分配readWrite
角色,授予他们对sales
数据库的读写访问权限。 现在没有为 sammy 分配其他角色,这意味着该用户在创建后不会立即拥有任何额外的访问权限。
注意:在如何保护MongoDB的先决条件教程中提到,passwordPrompt()
方法只兼容MongoDB版本4.2及更高版本。 如果您使用的是旧版本的 Mongo,那么您必须以明文形式写出该用户的密码,类似于您写出用户名的方式:
. . . pwd: "password", . . .
如果此方法成功,它将从 MongoDB shell 返回一条确认消息,类似于以下内容:
OutputSuccessfully added user: { "user" : "sammy", "roles" : [ { "role" : "readWrite", "db" : "sales" } ] }
您现在可以验证新用户是否可以登录到数据库,以及您指定的他们的访问权限是否得到正确执行。
您将保持当前 MongoDB shell 与您的管理用户登录以供以后打开,因此打开 一个单独的服务器会话 。
从新的服务器会话中,打开 MongoDB shell。 这次,指定 sammy 作为用户,sales
作为认证数据库:
mongo -u sammy -p --authenticationDatabase sales
输入创建sammy用户时设置的密码。 访问 shell 提示符后,执行 show dbs
命令列出可用的数据库:
show dbs
与您的管理帐户相比,sammy 只会列出一个数据库,因为您只授予他们访问 sales
数据库的权限。
Outputsales 0.000GB
现在检查 sammy 是否可以从 sales
数据库中的两个集合中检索对象。 切换到sales
数据库:
use sales
然后尝试检索所有客户:
db.customers.find()
此 find
命令将返回您在步骤 1 中在此集合中创建的文档:
Output{ "_id" : ObjectId("60d888946ae8ac2c9120ec40"), "name" : "Sammy" }
您还可以确认第二个集合 orders
是否按预期可用:
db.orders.find()
Output{ "_id" : ObjectId("60d890730d31cc50dedea6ff"), "total" : 100 }
为确保 sales
数据库的访问权限已正确配置,您可以检查 sammy 是否也可以插入新文档。 尝试插入新客户:
db.customers.insert({name: 'Ellie'})
因为您授予 sammy readWrite
角色,他们被授权将新文档写入此数据库。 MongoDB 将确认插入已成功完成:
OutputWriteResult({ "nInserted" : 1 })
最后,验证 sammy 是否可以访问 reports
数据库。 他们将无法读取或写入此数据库中的任何数据,因为您没有通过分配的角色授予他们访问权限。
切换到reports
数据库:
use reports
此 use
命令本身不会导致任何错误。 尝试通过运行以下命令访问您在步骤 1 中插入的文档:
db.reports.find()
现在,MongoDB 将抛出错误消息而不是返回任何对象:
OutputError: error: { "ok" : 0, "errmsg" : "not authorized on reports to execute command { find: \"reports\", filter: {}, lsid: { id: UUID(\"cca9e905-89f8-4903-ae12-46f23b43b967\") }, $db: \"reports\" }", "code" : 13, "codeName" : "Unauthorized" }
Unauthorized
错误信息告诉你 sammy 没有足够的访问权限来与 reports
数据库中的数据进行交互。
到目前为止,您已经创建了第一个具有有限权限的数据库用户,并验证了访问权限是否正确执行。 接下来,您将创建具有不同权限的第二个用户。
第三步——创建第二个用户
为销售代表 Sammy 创建了 sammy MongoDB 用户后,您仍然需要公司销售分析师 Joe 的帐户。 回想一下示例场景,Joe 的工作职能需要一组不同的数据库权限。
创建这个新用户帐户的过程类似于您创建 sammy 用户的过程。
返回到您的管理用户登录到 MongoDB shell 的服务器会话。 从那里切换到 reports
数据库:
use reports
因为 Joe 在报告部门工作,他们的 MongoDB 用户帐户将使用 reports
作为身份验证数据库创建。
使用以下命令创建新的 joe 用户:
db.createUser( { user: "joe", pwd: passwordPrompt(), roles: [ { role: "readWrite", db: "reports" }, { role: "read", db: "sales" } ] } )
请注意用于创建 joe 的方法与上一步中用于创建 sammy 的方法之间的差异。 这一次,您分配了两个不同的角色:
readWrite
应用于reports
数据库意味着 joe 将能够读取和写入该数据库的销售报告数据read
应用于sales
数据库确保 joe 可以访问销售数据,但不能将任何文档写入该数据库
这两个角色都是内置的 MongoDB 角色。
此命令将返回类似于以下内容的确认消息:
OutputSuccessfully added user: { "user" : "joe", "roles" : [ { "role" : "readWrite", "db" : "reports" }, { "role" : "read", "db" : "sales" } ] }
接下来,验证新用户的权限是否得到正确执行。
再次打开 另一个服务器会话 ,因为您将在后面的步骤中同时使用管理 MongoDB 用户和 sammy 用户。
打开 MongoDB shell,这次指定 joe 作为用户,reports
作为身份验证数据库:
mongo -u joe -p --authenticationDatabase reports
出现提示时,输入您在创建 joe 用户时设置的密码。 访问 shell 提示后,执行 show dbs
命令列出 joe 可用的数据库:
show dbs
由于 joe 可以同时使用 sales
和 reports
数据库,这两个数据库将在输出中列出:
Outputreports 0.000GB sales 0.000GB
现在您可以检查 joe 是否可以从 sales
数据库中检索对象。
切换到 sales
:
use sales
运行以下 find
命令尝试检索所有订单:
db.orders.find()
假设您正确设置了权限,此命令将返回您在步骤 1 中在此集合中创建的唯一文档:
Output{ "_id" : ObjectId("60d890730d31cc50dedea6ff"), "total" : 100 }
接下来,尝试在 orders
集合中插入一个新文档:
db.orders.insert({total: 50})
因为您只为该数据库分配了 joe 一个 read
角色,所以此 insert
命令将失败并显示错误消息:
OutputWriteCommandError({ "ok" : 0, "errmsg" : "not authorized on sales to execute command { insert: \"orders\", ordered: true, lsid: { id: UUID(\"ebbe853b-e269-463f-a1d4-2c5a5accb966\") }, $db: \"sales\" }", "code" : 13, "codeName" : "Unauthorized" })
Unauthorized
消息告诉您失败背后的原因 - joe 拥有的访问权限不足以插入新文档。
接下来确认joe是否可以读写reports
数据库中的数据。
切换到 reports
:
use reports
然后,尝试使用 find
命令访问其中的数据:
db.reports.find()
由于允许从数据库中读取 joe,因此 MongoDB 将使用此集合中当前可用文档的列表进行响应:
Output{ "_id" : ObjectId("60d8897d6ae8ac2c9120ec41"), "orders" : 1 }
然后尝试通过运行以下命令插入新报告:
db.reports.insert({orders: 2})
此命令也将成功,输出消息类似于:
OutputWriteResult({ "nInserted" : 1 })
这样,您就创建了具有有限权限的第二个数据库用户,但这次您将角色授予了两个单独的数据库。 您还验证了他们的访问权限是否由数据库服务器正确实施。 接下来,您将向现有用户之一授予然后撤销其他权限。
第 4 步 — 授予和撤销现有用户的角色
在步骤 2 和 3 中,您创建了新用户并在创建过程中为他们分配了角色。 在实践中,数据库管理员经常需要撤销或授予系统中已经创建的用户新的权限。 在此步骤中,您将授予 sammy 用户一个新角色,以允许他们访问 reports
数据库并在不久后撤销该权限。
从 管理 shell ,切换到创建用户 sammy 的 sales
数据库:
use sales
要验证用户 sammy 是否存在,请执行 show users
命令:
show users
此命令将返回此数据库中所有用户的列表以及他们各自的角色:
Output{ "_id" : "sales.sammy", "userId" : UUID("cbc8ac18-37d8-4531-a52b-e7574044abcd"), "user" : "sammy", "db" : "sales", "roles" : [ { "role" : "readWrite", "db" : "sales" } ], "mechanisms" : [ "SCRAM-SHA-1", "SCRAM-SHA-256" ] }
要为该用户分配新角色,您可以使用 grantRolesToUser
方法。 此方法接受用户名和要授予的角色列表,其语法与创建新用户时使用的语法相同。
此步骤的目标是授予 sammy 对 reports
数据库的只读权限,因此为他们分配该数据库的 read
角色:
db.grantRolesToUser("sammy", [{role: "read", db: "reports"}])
除非出现错误,否则该命令不会产生任何输出,因此缺少任何消息是预期的行为。 再次执行show users
命令可以验证命令是否生效:
show users
此命令将返回类似于以下内容的输出:
Output{ "_id" : "sales.sammy", "userId" : UUID("cbc8ac18-37d8-4531-a52b-e7574044abcd"), "user" : "sammy", "db" : "sales", "roles" : [ { "role" : "read", "db" : "reports" }, { "role" : "readWrite", "db" : "sales" } ], "mechanisms" : [ "SCRAM-SHA-1", "SCRAM-SHA-256" ] }
请注意 roles
部分中新增的角色。
现在您可以检查 sammy 在更改后是否确实能够访问 reports
数据库。 切换到 sammy 登录的终端窗口,然后再次尝试访问报告。
切换到reports
数据库:
use reports
然后在 reports
集合上运行 find
命令:
db.reports.find()
上次,该命令失败并显示错误消息。 不过,这一次,它将返回 reports
数据库中的文档列表:
Output{ "_id" : ObjectId("60d8897d6ae8ac2c9120ec41"), "orders" : 1 } { "_id" : ObjectId("60d899cafe3d26bf80e947fd"), "orders" : 2 }
一段时间后,您可能想要撤销 sammy 用户访问报告的能力。 为了说明这一点,回到 管理控制台 并执行以下命令,这将撤销 sammy 用户对 reports
数据库的 read
权限:
db.revokeRolesFromUser("sammy", [{role: "read", db: "reports"}])
revokeRolesFromUser
方法采用与 grantRolesToUser
相同的参数集,但删除了指定的角色。
再一次,您可以验证该角色不再可用于 show users
:
Output{ "_id" : "sales.sammy", "userId" : UUID("cbc8ac18-37d8-4531-a52b-e7574044abcd"), "user" : "sammy", "db" : "sales", "roles" : [ { "role" : "readWrite", "db" : "sales" } ], "mechanisms" : [ "SCRAM-SHA-1", "SCRAM-SHA-256" ] }
要仔细检查 sammy 是否无法再从 reports
数据库中读取,请尝试在已记录 sammy 的 shell 中重新运行之前的 find
命令在:
db.reports.find()
这次命令将再次失败并出现 Unauthorized
错误:
OutputError: error: { "ok" : 0, "errmsg" : "not authorized on reports to execute command { find: \"reports\", filter: {}, lsid: { id: UUID(\"2c86fba2-7615-40ae-9c3b-2dfdac2ed288\") }, $db: \"reports\" }", "code" : 13, "codeName" : "Unauthorized" }
这表明您撤销了 sammy 用户的 read
角色成功。
结论
在本文中,您学习了如何创建对数据库具有有限访问权限的用户,并使用基于角色的访问控制在您的数据库上实施最小权限原则,只向用户授予最少的必要权限集。 您还学习了如何授予和撤销现有用户的角色,学习如何在必要的权限随时间变化时管理活动数据库服务器上的访问权限,并验证更改是否立即生效。
使用 MongoDB 的基于角色的访问控制,您可以使用用户定义的角色等功能定义精确的访问级别,在内置角色不满意时创建自定义角色,以及允许管理员授予用户对特定集合的权限的集合级访问控制而不是整个数据库。 我们鼓励您详细了解 官方 MongoDB 文档 中详细描述的这些和其他基于角色的访问控制功能。