限制

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

安全和索引

Cypher 手册 → 用于搜索性能的索引 中所述,Neo4j 5 支持创建和使用索引来提高 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)。因此,由于安全检查所需的额外数据访问,与以不受限制的用户身份执行查询相比,此操作将更慢。