限制

本节描述了 Neo4j 基于角色的访问控制安全的已知限制和影响。

安全与索引

Cypher 手册 → 用于搜索性能的索引中所述,Neo4j 2025.05 支持创建和使用索引来提高 Cypher 查询的性能。

请注意,无论是否使用索引,Neo4j 安全模型都会影响查询结果。当使用非全文 Neo4j 索引时,Cypher 查询将始终返回与没有索引时相同的结果。这意味着,如果安全模型由于 图和子图访问控制 中受限的读取访问而导致返回较少结果,则索引也将返回相同的较少结果。

但是,此规则并非完全适用于 Cypher 手册 → 用于全文搜索的索引。这些特定索引内部由 Lucene 支持。因此,无法确定安全违规是否影响了从索引返回的每个特定条目。鉴于此,如果确定任何结果可能违反该查询有效的安全权限,Neo4j 将从全文索引返回零结果。

由于全文索引不会被 Cypher 自动使用,因此它们不会导致仅仅因为创建了此类索引而使相同的 Cypher 查询返回不同结果的情况。用户需要显式调用存储过程来使用这些索引。问题仅在于,如果用户不知道此行为,他们可能会期望全文索引返回与不同但语义相似的 Cypher 查询相同的结果。

拒绝属性示例

考虑以下示例。数据库具有带有标签 :User:Person 的节点,它们具有 namesurname 属性。这两个属性上都有索引

CREATE INDEX singleProp FOR (n:User) ON (n.name);
CREATE INDEX composite  FOR (n:User) ON (n.name, n.surname);
CREATE FULLTEXT INDEX userNames FOR (n:User|Person) ON EACH [n.name, n.surname];

全文索引支持多个标签。有关创建和使用全文索引的更多详细信息,请参阅 Cypher 手册 → 用于全文搜索的索引

创建这些索引后,后两个索引似乎实现了相同的功能。然而,这并不完全准确。复合索引和全文索引的行为方式不同,并且专注于不同的用例。一个主要区别是全文索引由 Lucene 支持,并将使用 Lucene 语法进行查询。

这对受索引中涉及的标签或属性限制的用户会产生影响。理想情况下,如果索引中的标签和属性被拒绝,它们可以正确地从原生索引和全文索引返回零结果。但是,在某些临界情况下,情况并非如此简单。

假设以下节点已添加到数据库中

CREATE (:User {name: 'Sandy'});
CREATE (:User {name: 'Mark', surname: 'Andy'});
CREATE (:User {name: 'Andy', surname: 'Anderson'});
CREATE (:User:Person {name: 'Mandy', surname: 'Smith'});
CREATE (:User:Person {name: 'Joe', surname: 'Andy'});

考虑拒绝标签 :Person

DENY TRAVERSE ON GRAPH * NODES Person TO users

如果用户运行使用 name 上的原生单属性索引的查询

MATCH (n:User) WHERE n.name CONTAINS 'ndy' RETURN n.name

此查询执行多项检查

  • 扫描索引以创建具有 name 属性的节点的结果流,从而得到五个结果。

  • 过滤结果以仅包含 n.name CONTAINS 'ndy' 的节点,排除 MarkJoe,从而得到三个结果。

  • 过滤结果以排除同时具有被拒绝标签 :Person 的节点,排除 Mandy,从而得到两个结果。

此数据集将返回两个结果,其中只有一个具有 surname 属性。

为了使用 namesurname 上的原生复合索引,查询还需要包含一个关于 surname 属性的谓词

MATCH (n:User)
WHERE n.name CONTAINS 'ndy' AND n.surname IS NOT NULL
RETURN n.name

此查询执行多项检查,这些检查与单属性索引查询几乎相同

  • 扫描索引以创建具有 namesurname 属性的节点的结果流,从而得到四个结果。

  • 过滤结果以仅包含 n.name CONTAINS 'ndy' 的节点,排除 MarkJoe,从而得到两个结果。

  • 过滤结果以排除同时具有被拒绝标签 :Person 的节点,排除 Mandy,从而得到一个结果。

上述数据集只返回了一个结果。如果改用带有全文索引的此查询会怎样

CALL db.index.fulltext.queryNodes("userNames", "ndy") YIELD node, score
RETURN node.name

现在的问题是,不确定索引提供的结果是由于匹配 name 属性还是 surname 属性而获得的。查询引擎将执行的步骤是

  • 在全文索引上运行 Lucene 查询,以在任一属性中生成包含 ndy 的结果,从而得到五个结果。

  • 过滤结果以排除同时具有标签 :Person 的节点,排除 MandyJoe,从而得到三个结果。

这种结果差异是由索引创建中两个属性之间的 OR 关系引起的。

拒绝属性

现在考虑拒绝访问属性,例如 surname 属性

DENY READ {surname} ON GRAPH * TO users

为此,再次运行相同的查询

MATCH (n:User)
WHERE n.name CONTAINS 'ndy'
RETURN n.name

此查询与之前完全相同,返回相同的两个结果,因为其中没有任何内容与被拒绝的属性相关。

但是,对于针对复合索引的查询来说,情况并非如此

MATCH (n:User)
WHERE n.name CONTAINS 'ndy' AND n.surname IS NOT NULL
RETURN n.name

由于 surname 属性被拒绝,它将始终显示为 null,并且复合索引为空。因此,查询不返回任何结果。

现在考虑全文索引查询

CALL db.index.fulltext.queryNodes("userNames", "ndy") YIELD node, score
RETURN node.name

问题依然存在,因为不确定索引提供的结果是由于匹配 name 属性还是 surname 属性而返回的。现在,来自 surname 属性的结果需要通过安全规则排除,因为它们要求用户无法看到任何 surname 属性。然而,安全模型无法内省 Lucene 查询以了解它将实际做什么,无论是仅作用于允许的 name 属性,还是也作用于不允许的 surname 属性。已知的是,先前的查询返回了与 Joe Andy 的匹配,现在应该将其过滤掉。因此,为了永不返回用户不应该看到的结果,所有结果都需要被阻止。查询引擎将采取的步骤是

  • 确定全文索引是否包含被拒绝的属性。

  • 如果是,则返回一个空的结果流。否则,它将按之前所述进行处理。

在这种情况下,查询将返回零结果,而不是仅仅返回预期可能出现的 AndySandy 的结果。

安全与标签

遍历具有多标签的图节点

访问控制权限对图遍历的总体影响在 图和子图访问控制 中有详细描述。以下部分将仅关注节点,因为它们能够拥有多个标签。关系只能有一种类型的标签,因此它们不表现本节旨在阐明的行为。虽然本节不会进一步提及关系,但遍历权限的一般功能也适用于它们。

对于任何可遍历的节点,由于 GRANT TRAVERSEGRANT MATCH,用户可以通过调用内置的 labels() 函数获取有关附加标签的信息。在具有多个标签的节点的情况下,它们可以返回给未直接授予访问权限的用户。

为了举例说明,假设一个图中有三个节点:一个标签为 :A,另一个标签为 :B,还有一个同时带有标签 :A:B。在这种情况下,有一个用户具有由以下定义的角色 custom

GRANT TRAVERSE ON GRAPH * NODES A TO custom

如果该用户执行

MATCH (n:A)
RETURN n, labels(n)

他们将得到一个包含两个节点的结果:一个带有 :A 标签的节点,以及一个带有 :A :B 标签的节点。

相比之下,执行

MATCH (n:B)
RETURN n, labels(n)

这将只返回同时具有 :A:B 两个标签的节点。即使 :B 没有遍历访问权限,但由于允许列表中的标签 :A 附加到同一节点,数据集中仍有一个带有该标签的节点可访问。

如果用户被拒绝遍历某个标签,他们将永远不会从任何附加有此标签的节点中获取结果。因此,标签名称将永远不会对他们显示。例如,可以通过执行以下操作来实现

DENY TRAVERSE ON GRAPH * NODES B TO custom

查询

MATCH (n:A)
RETURN n, labels(n)

现在将只返回带有 :A 标签的节点,而查询

MATCH (n:B)
RETURN n, labels(n)

现在将不返回任何节点。

db.labels() 存储过程

与上一节中描述的普通图遍历不同,内置的 db.labels() 存储过程不处理数据图本身,而是处理系统图上定义的安全规则。这意味着

  • 如果一个标签被明确列入白名单(授予),它将由这个存储过程返回。

  • 如果一个标签被拒绝或未被明确允许,它将不会由这个存储过程返回。

重复使用上一个示例,假设一个图中有三个节点:一个标签为 :A,另一个标签为 :B,还有一个同时带有标签 :A:B。在这种情况下,有一个用户具有由以下定义的角色 custom

GRANT TRAVERSE ON GRAPH * NODES A TO custom

这意味着只有标签 :A 被明确列入允许列表。因此,执行

CALL db.labels()

将只返回标签 :A,因为这是唯一被授予遍历权限的标签。

针对不存在的标签、关系类型和属性名称的权限

针对不存在的标签、关系类型和属性名称的权限仅在它们被创建后才生效。换句话说,在授权用户时,只应用针对现有标签、关系类型和属性名称的权限。这是因为图元素必须在内部解析,以便在用户稍后尝试使用它们时能够对照权限进行检查。如果标签、关系类型或属性名称尚不存在,它将无法解析,因此权限将不适用。

解决此问题的一种方法是在创建权限时,使用相关数据库上的 db.createLabel()db.createRelationshipType()db.createProperty() 存储过程来创建标签、关系类型或属性名称。

如果满足以下条件,则标签、关系类型和属性名称在数据库中被视为不存在:

  • 从未存在过带有该标签的节点、带有该关系类型的关系或带有该名称的属性。

  • 从未尝试添加带有该标签的节点、带有该关系类型的关系或带有该名称的属性。
    即使创建本身失败(除非失败是由于缺少或拒绝创建新标签、关系类型或属性名称的权限),尝试创建也会将其添加到已知的标签、关系类型和属性名称中。

  • 它们尚未使用 db.createLabel()db.createRelationshipType()db.createProperty() 存储过程创建。

目前无法从数据库中删除标签、关系类型或属性名称。一旦在数据库中存在,它们就无法恢复到不存在的状态。

例如,假设您有一个新创建的空数据库,名为 testing,以及一个名为 Alice 的用户,其角色为 custom

本示例仅关注节点及其标签,但相同的原则适用于关系及其关系类型,以及属性(在节点和关系上)及其名称。

使用以下命令,您为 custom 角色定义了一些权限

GRANT MATCH {*} ON GRAPH testing NODES * TO custom
GRANT CREATE ON GRAPH testing NODES `A` TO custom
GRANT SET LABEL `A` ON GRAPH testing TO custom
GRANT CREATE NEW NODE LABEL ON DATABASE testing TO custom

这意味着当 Alice 执行

CREATE (:`A`)

她将收到以下异常,尽管她被允许创建新标签

Create node with labels 'A' on database 'testing' is not allowed for user 'Alice' with roles [PUBLIC, custom].

然而,重新运行相同的查询将创建该节点。这是因为即使创建失败,标签仍然会被创建,这使得在第二次运行查询时它不再是“不存在”的。

为确保首次尝试成功,在为 custom 角色设置权限时,管理员应在受影响的数据库上对所有被分配权限但尚不存在的标签运行 db.createLabel() 存储过程。在此示例中,创建自定义角色时,连接到 testing 并运行 CALL db.createLabel('A') 以确保 Alice 在首次尝试时成功创建节点。

安全与性能

安全模型的规则可能会影响某些数据库操作的性能。这是因为需要额外的安全检查,并且这些检查需要额外的数据访问。例如,通常是快速查找的计数存储操作,可能会在性能方面出现显著差异。

以下示例展示了将安全规则添加到角色 restrictedunrestricted 时数据库的行为

GRANT TRAVERSE ON GRAPH * NODES Person TO restricted;
DENY TRAVERSE ON GRAPH * NODES Customer TO restricted;
GRANT TRAVERSE ON GRAPH * ELEMENTS * TO unrestricted;

现在,让我们看看数据库需要做什么才能执行以下查询

MATCH (n:Person)
RETURN count(n)

对于这两个角色,执行计划如下所示

+--------------------------+
| Operator                 |
+--------------------------+
| +ProduceResults          |
| |                        +
| +NodeCountFromCountStore |
+--------------------------+

然而,在内部,需要执行的操作却大相径庭。下表说明了这种差异

具有 unrestricted 角色的用户 具有 restricted 角色的用户

数据库可以访问计数存储并检索带有标签 :Person 的节点总数。

这是一个非常快速的操作。

数据库无法访问计数存储,因为它必须确保只计算带有所需标签 :Person 的可遍历节点。因此,需要访问并检查每个带有 :Person 标签的节点,以确保它们不包含被拒绝的标签,例如 :Customer

因此,由于安全检查所需的额外数据访问,此操作将比作为不受限制的用户执行查询慢。

基于属性的访问控制限制

当基于属性规则添加安全规则时,需要进行额外的节点或关系级安全检查,这可能会对性能产生显著影响。

以下示例展示了将节点的安全规则添加到角色 restrictedunrestricted 时数据库的行为。相同的限制也适用于关系。

GRANT TRAVERSE ON GRAPH * FOR (n:Customer) WHERE n.secret <> true TO restricted;
GRANT TRAVERSE ON GRAPH * ELEMENTS * TO unrestricted;

执行查询时

MATCH (n:Customer)
RETURN n

对于这两个角色,执行计划如下所示

+--------------------------+
| Operator                 |
+--------------------------+
| +ProduceResults          |
| |                        +
| +AllNodesScan             |
+--------------------------+

然而,在内部,需要执行的操作却大相径庭。下表说明了这种差异

具有 unrestricted 角色的用户 具有 restricted 角色的用户

数据库将扫描所有节点,并仅根据 :Customer 标签的存在快速识别可访问的节点。这是一个相对较快的操作。

数据库将扫描所有节点,根据指定标签的存在识别潜在可访问节点,然后还将访问每个节点的属性并检查其值,以确保满足属性规则条件(即,在此情况下 secret 未设置为 true)。因此,由于安全检查所需的额外数据访问,此操作将比作为不受限制的用户执行查询慢。

© . All rights reserved.