限制

Neo4j 的基于角色的访问控制(RBAC)存在一些限制,如果在设计安全模型或编写 Cypher 查询时未充分考虑这些限制,可能会导致意想不到的结果。根据用户所分配角色所受的限制,查询结果可能会在以下方面有所不同:

  • 访问控制与索引: 索引用于加速查询,但索引返回给用户的内容取决于安全模型。如果用户对某些标签、关系类型或属性有限制,系统将不会返回任何包含这些元素的查询结果,无论使用了什么索引。此外,全文索引和向量索引返回的结果可能少于预期,因为 Lucene 机制使得 Neo4j 无法针对每个返回的索引条目检查安全规则。因此,Neo4j 仅返回能够确保不违反安全规则的结果,并排除可能违反安全规则的结果(即使实际上并未违反)。

  • 失效开放(Fail-open)的 DENY 行为:DENY(拒绝)规则的条件未满足时,它会失效开放,Neo4j 不会应用该限制。如果存在更广泛的 GRANT(授权)规则,系统将默认授予访问权限。如果 DENY 规则设计不当,可能导致意外的数据泄露。

  • 访问控制与标签: 当节点具有多个标签,且用户仅有权遍历其中部分标签时,查询结果会有所不同。这是因为用户可以看到他们有权访问的节点上附加的所有标签,即使他们并未获得遍历所有这些标签的权限。

  • 访问控制与性能: 复杂的安全规则和具有限制性访问的大型图可能会导致性能显著下降,特别是对于本应高效的查询而言。

访问控制与索引

Neo4j 使用索引来加速 Cypher 查询。有关 Neo4j 中可用索引类型的详细信息,请参阅 Cypher 手册 → 索引

然而,无论是否使用索引,Neo4j 的安全模型始终控制着您所能看到的结果。

搜索性能索引(范围、文本、点和令牌查找)

搜索性能索引在与查询相关时会被 Cypher 规划器自动使用。这些索引返回的结果会经过安全模型过滤,确保用户只能看到他们有权访问的结果。如果由于 图和子图访问控制 中的读取限制导致安全模型返回的结果较少,索引也将返回同样较少的结果。

语义索引(全文和向量)

全文索引向量索引与搜索性能索引不同,因为它们基于 Lucene 构建,且不会被 Cypher 规划器自动使用。

Lucene 的影响

Lucene 阻止了 Neo4j 对从索引返回的每个特定条目进行安全规则检查。因此,Neo4j 需要采取更保守的方法,以确保不返回违反安全规则的结果。如果一个结果可能违反安全规则,Neo4j 会将其视为确实违反,并将其从索引返回的结果中排除。这意味着它要么返回零结果(如果索引中的所有结果都可能受到影响),要么返回部分结果(如果可以保证某些行不受影响)。

使用建议

Cypher 不会自动使用全文索引或向量索引。您在调用全文索引的过程(如 db.index.fulltext.queryNodesdb.index.fulltext.queryRelationships)或向量索引的过程(如 db.index.vector.queryNodesdb.index.vector.queryRelationships)时,或通过 Cypher 的 SEARCH 子句(Neo4j 2026.01 引入)时,必须显式声明要使用的索引。这避免了同一个 Cypher 查询因底层索引不同而返回不同结果的情况。问题在于,如果您不了解此行为,可能会期望全文索引或向量索引返回与语义相似的 Cypher 查询相同的结果。

以下示例说明了在使用不同安全规则时使用全文索引与搜索性能索引的影响。全文索引的示例也适用于向量索引。具有多个标签、多个关系类型或附加属性的向量索引于 Neo4j 2026.01 中引入。在以前的版本中,它们只能基于单个属性和单个标签或关系类型。

示例 1:拒绝的标签与多属性索引

该示例假设数据库包含标签为 :User:Person 的节点,以及属性 namesurname

  1. name 上创建一个单属性范围索引,在 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 语法进行查询。这对被限制了索引中涉及的标签或属性的用户有影响。理想情况下,如果索引中的标签和属性被拒绝,原生索引和全文索引都应正确地返回零结果。然而,在某些边缘情况下,情况并非如此简单。

  2. 让我们向数据库添加以下节点

    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'});
  3. 拒绝 :Person 标签

    DENY TRAVERSE ON GRAPH * NODES Person TO users;
  4. 现在运行一个使用其中一个索引的查询

    运行一个使用 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 关系引起的。

示例 2:拒绝所有标签上的属性

该示例使用与 示例 1 相同的数据库,但不是拒绝 :Person 标签,而是拒绝所有标签上的 surname 属性。

  1. 拒绝所有标签上的 surname 属性

    DENY READ {surname} ON GRAPH * TO users;
  2. 使用其中一个索引运行与前一个示例相同的查询

    运行一个使用 name 上单属性范围索引的查询

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

    此查询的运行方式与之前完全相同,除了对 :Person 标签的检查,因为它与被拒绝的属性无关。

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

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

    最终,该查询返回三个结果,其中只有一个具有 surname 属性。

    运行一个使用 namesurname 复合范围索引的查询

    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 属性。因此,为防止索引返回用户不应看到的结果,索引会将所有属性视为已拒绝,而不仅仅是 surname 属性。查询执行以下步骤:

    • 在全文索引上运行 Lucene 查询,返回在任一属性中包含 ndy 的结果,得出五个结果。

    • 过滤结果以排除所有节点,过滤掉所有人,导致没有结果。

    最终,查询返回零结果,而不是预期的 AndyMandySandy

示例 3:拒绝特定标签上的属性

考虑仅对具有 :Person 标签的节点拒绝读取 surname 属性。

  1. 拒绝读取具有 :Person 标签的节点的 surname 属性

    DENY READ {surname} ON GRAPH * NODES Person TO users;
  2. 使用其中一个索引运行与前述示例相同的查询

    运行一个使用 name 上单属性范围索引的查询

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

    查询的操作与 示例 2 完全相同,返回相同的三个结果,因为该查询与被拒绝的属性无关。

    运行一个使用 namesurname 复合范围索引的查询

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

    由于 surname 属性仅对具有 :Person 标签的节点被拒绝,因此它仅对这些节点表现为 null,复合索引不包含这些节点。复合索引仍然包含其余具有这两个属性的 :User 节点。因此,查询仅返回一个结果。

    运行一个在 namesurname 上使用全文索引的查询

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

    现在的问题是,不清楚索引提供的结果是匹配了 name 属性还是 surname 属性。然而,影响有所减轻,因为仅需要从 surname 属性的结果中排除 :Person 标签的节点。没有 :Person 标签的节点可以读取 surname 属性,因此无需被排除。

    安全模型无法检查 Lucene 查询以确定其具体操作。因此,为防止索引返回用户不应看到的结果,索引会将具有 :Person 标签的节点的所有属性视为已拒绝。由于 surname 属性仅对具有 :Person 标签的节点被拒绝,索引可以安全地返回任何没有该标签的节点。

    查询执行以下步骤:

    • 在全文索引上运行 Lucene 查询,返回在任一属性中包含 ndy 的结果,得出五个结果。

    • 过滤结果,排除同时也具有 :Person 标签的节点,过滤掉 MandyJoe,得出三个结果。

    最终,查询返回三个结果而不是零,因为安全模型仍然允许来自没有 :Person 标签的节点的结果,即使它们在被拒绝的 surname 属性上匹配。

失效开放的 DENY 行为

DENY 规则的条件未满足时,它会失效开放,Neo4j 不会应用该限制。如果存在更广泛的 GRANT,系统将默认授予访问权限。如果 DENY 规则设计不当,可能导致意外的数据泄露。为避免这种情况,请遵循最小权限原则,仅授予用户查看特定数据所需的权限。

例如,考虑以下场景:

基于属性的 RBAC 中未满足的 DENY 导致失效开放

您授予用户对某个属性的访问权限,并试图通过 DENY 规则进行限制。然而,如果 DENY 规则未匹配任何数据(例如,属性为 null 或拼写错误),该规则将不会生效,用户仍然可以访问该属性。

让我们看几个示例:

示例 1. 授予所有具有 Employee 标签的节点上 salary 属性的读取访问权限,但对于职位为 'CEO' 的员工拒绝该权限。
GRANT READ {salary} ON GRAPH * NODES Employee TO myRole;
DENY READ {salary} ON GRAPH * FOR (e:Employee) WHERE e.position = 'CEO' TO myRole;

在这种情况下,如果 e.position 属性为 null 或拼写错误,DENY 规则将不会应用,myRole 仍然会看到 salary 属性。

更好的方法是应用最小权限原则,仅对职位不是 'CEO' 的员工授予 salary 属性的访问权限。

示例 2. 仅对所有具有 Employee 标签且职位不是 'CEO' 的节点,授予 salary 属性的读取访问权限。
GRANT READ {salary} ON GRAPH * FOR (e:Employee) WHERE e.position <> 'CEO' TO myRole;

这样,如果 e.position 为 null 或拼写错误,用户将不会看到 salary 属性,且不需要使用 DENY

或者,如果必须使用 DENY,可以通过添加额外的 DENY 来涵盖 e.position 为 null 的情况。

示例 3. 拒绝职位为 null 的员工的 salary 属性读取访问权限。
DENY READ {salary} ON GRAPH * FOR (e:Employee) WHERE e.position IS NULL TO myRole;

这样,如果 e.position 为 null,用户将看不到 salary 属性。

或者,您可以添加约束以确保 e.position 属性不能为空,从而使 DENY 条件始终可检查。

示例 4. 添加约束以确保 position 属性不能为空。
CREATE CONSTRAINT FOR (e:Employee) REQUIRE e.position IS NOT NULL;

这样,DENY 永远不会因 null 值而失效,用户将无法看到职位为 'CEO' 的员工的 salary 属性。

基于标签的 RBAC 中未满足的 DENY 导致失效开放

DENY 规则过于宽泛且未匹配数据时,它不会生效。

例如,如果您授予对所有节点上 salary 属性的读取访问权限,然后试图通过针对特定标签的 DENY 规则进行限制,如果图中不存在带有该标签的节点,则 DENY 规则不会应用,用户仍然可以访问所有节点上的 salary 属性。

示例 5. 授予对所有节点上 salary 属性的读取访问权限。
GRANT READ {salary} ON GRAPH * NODES * TO myRole;

这授予了对所有节点(包括不应被访问的节点)上 salary 属性的读取权限。

然后,您尝试用 DENY 规则限制对标注为 Management 的节点的 salary 属性访问。

示例 6. 拒绝具有 Management 标签的节点的 salary 属性读取权限。
DENY READ {salary} ON GRAPH * NODES Management TO myRole;

在这种情况下,如果 Management 标签不存在于具有 salary 属性的节点上,DENY 规则不适用,myRole 仍然可以看到该属性。

更好的方法是应用最小权限原则,仅对具有特定标签(如 IndividualContributor)的节点授予 salary 属性的访问权限。

示例 7. 仅对具有 IndividualContributor 标签的节点授予 salary 属性的读取访问权限。
GRANT READ {salary} ON GRAPH * NODES IndividualContributor TO myRole;

这样,用户仅在具有 IndividualContributor 标签的节点上看到 salary 属性。

访问控制与标签

在 Neo4j 中,节点可以有多个标签,但关系只能有一种类型。在控制访问权限时,这一点很重要。

以下场景仅关注节点,因为它们可以有多个标签。同样的一般规则适用于关系,但关系比较简单。

有关访问控制权限对图遍历影响的详细信息,请参阅 图和子图访问控制

遍历具有部分标签访问权限的多标签节点

要通过调用内置的 labels() 函数获取节点标签信息,用户需要具有使用 GRANT TRAVERSEGRANT MATCH 遍历该节点上一个或多个标签的权限。对于多标签节点,即使未被授予遍历其中某些标签的权限,用户仍能看到该节点上附加的所有标签。

例如,假设一个图包含三个节点:一个标注为 :A,一个标注为 :B,一个同时标注为 :A:B

  1. custom 角色授予对 :A 标签节点的遍历访问权限。

    GRANT TRAVERSE ON GRAPH * NODES A TO custom;
  2. 然后运行以下查询之一查看结果:

    运行查询获取所有带有 :A 标签节点的标签。

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

    查询返回两个节点:具有 :A 标签的节点和具有 :A :B 标签的节点。

    相比之下,如果运行查询获取 :B 标签节点的标签:

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

    查询仅返回具有两个标签(:A:B)的节点。尽管该节点在 :B 上没有遍历访问权限,但由于该节点同时具有被允许的 :A 标签,因此在数据集中可见。

遍历对某些标签被拒绝访问的多标签节点

如果用户被拒绝访问某个标签,他们将永远不会从任何带有该标签的节点获取结果。因此,该标签名称对他们来说永远不可见。

例如,假设一个图包含三个节点:一个标注为 :A,一个标注为 :B,一个同时标注为 :A:B。用户有权遍历 :A 标签,但没有权遍历 :B 标签。

  1. 授予 custom 角色对 :A 标签节点的遍历权限,但拒绝 :B 标签的遍历权限。

    GRANT TRAVERSE ON GRAPH * NODES A TO custom;
    DENY TRAVERSE ON GRAPH * NODES B TO custom;
  2. 然后运行以下查询之一查看结果:

    运行查询获取所有带有 :A 标签节点的标签。

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

    查询仅返回具有 :A 标签的节点。它不会返回同时具有 :A:B 的节点,因为用户由于 :B 的存在而没有遍历该节点的权限,即使它也具有被允许的 :A 标签。

    运行查询获取 :B 标签节点的标签:

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

    查询不返回任何节点。

db.labels() 过程

与前述常规图遍历不同,内置的 db.labels() 过程不处理数据图本身,而是处理定义在系统图上的安全规则。这意味着:

  • 如果一个标签被显式允许(granted),它将由该过程返回。

  • 如果一个标签被拒绝或未显式允许,它将不会由该过程返回。

例如,假设一个图包含三个节点:一个标注为 :A,一个标注为 :B,一个同时标注为 :A:B

  1. custom 角色授予对 :A 标签节点的遍历访问权限。

    GRANT TRAVERSE ON GRAPH * NODES A TO custom;
  2. 运行以下查询:

    CALL db.labels()

    查询仅返回 :A 标签,因为用户没有遍历 :B 标签的权限。

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

对于不存在的标签、关系类型和属性名称的权限仅在它们创建后生效。在授权用户时,仅应用现有标签、关系类型和属性名称的权限。这是因为在用户后续使用时,图元素必须在内部解析以进行权限检查。如果标签、关系类型或属性名称尚不存在,则无法解析,因此权限不会应用。

解决方法是在创建权限时,使用 db.createLabel()db.createRelationshipType()db.createProperty() 过程在相关数据库中显式创建它们。

如果以下情况发生,则标签、关系类型或属性名称在数据库中被视为不存在:

  • 从来没有节点带有该标签、关系带有该关系类型或属性带有该名称,也没有针对它们的索引或约束。

  • 没有尝试添加带有该标签的节点、该关系类型的关系或该名称的属性。尝试创建操作会将名称添加到已知列表中(即使操作最终失败,除非失败原因是缺乏权限)。

  • 没有尝试创建使用该标签、关系类型或属性名称的索引或约束。

  • 未使用任何 db.createLabel() 等过程创建它们。

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

例如,假设您有一个名为 testing 的新空数据库。

本示例仅关注节点及其标签,但同样原则适用于关系类型和属性名称。

  1. custom 角色定义包含不存在的 :A 标签的权限。

    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;
  2. Alicecustom 角色)身份登录并运行查询以创建带有 :A 标签的节点。

    CREATE (:A)

    尽管您有权创建新标签,但仍会收到异常。

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

然而,再次运行相同的查询会成功创建节点。这是因为失败的尝试已经创建了标签,使其在第二次查询时不再属于“不存在”。

为了确保第一次尝试成功,管理员应在设置权限时,通过 db.createLabel() 在相关数据库中预先创建所有不存在的标签。

访问控制与性能

Neo4j 的 RBAC 可能会带来性能影响,特别是在安全规则复杂或查询大型图时。例如,count store(计数存储)操作本应是快速查找,但因为数据库必须针对安全规则检查每个节点或关系,性能可能会显著下降。

数据库操作的访问控制与性能

当存在限制对特定节点、关系或属性访问的安全规则时,Neo4j 必须执行额外检查。

例如,考虑一个具有 :Person:Customer 标签的图。

  1. 定义两个具有不同权限的角色:

    • restricted — 遍历受限。

    • unrestricted — 遍历不受限。

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

      restricted 角色允许遍历 :Person,但拒绝 :Customer

  2. 运行查询时,执行计划看起来相同:

    统计 :Person 标签节点的 Cypher 查询。
    MATCH (n:Person)
    RETURN count(n)
    两个用户的执行计划一致。
    +--------------------------+
    | Operator                 |
    +--------------------------+
    | +ProduceResults          |
    | |                        +
    | +NodeCountFromCountStore |
    +--------------------------+

    然而,底层的操作可能会有显著差异:

unrestricted 用户: restricted 用户:

数据库访问计数存储以检索 :Person 节点的总数,无需检查安全规则,速度非常快。

数据库无法利用计数存储,因为它必须确保只统计 :Person 且未被拒绝(即不是 :Customer)的节点。它需要检查每个 :Person 节点是否也具有 :Customer 标签。这增加了数据访问量,导致操作较慢。

属性规则的访问控制与性能

基于属性的规则需要额外的安全检查,这会产生显著的性能影响。

以下示例展示了数据库行为。

  1. 定义两个具有不同权限的角色:

    GRANT TRAVERSE ON GRAPH * FOR (n:Customer) WHERE n.secret <> true TO restricted;
    GRANT TRAVERSE ON GRAPH * ELEMENTS * TO unrestricted;
  2. 运行查询时,执行计划看起来相同:

    获取所有 :Customer 节点的 Cypher 查询。
    MATCH (n:Customer)
    RETURN n
    任一角色的执行计划。
    +--------------------------+
    | Operator                 |
    +--------------------------+
    | +ProduceResults          |
    | |                        +
    | +AllNodesScan             |
    +--------------------------+

    然而,底层的操作可能会有显著差异:

unrestricted 用户: restricted 用户:

数据库扫描所有节点并根据标签识别,操作相对较快。

数据库扫描节点,识别潜在可访问节点,然后访问它们的属性值以确保满足规则(即 secret 不为 true)。额外的访问导致该操作比无限制用户慢。

© . This site is unofficial and not affiliated with Neo4j, Inc.