架构
架构通过指令实现 IFTTT

通过指令实现 IFTTT

Gato GraphQL 提供了通过指令实现 IFTTT(If This Then That)策略的能力。这些指令会在 query 中出现特定字段或指令时,被动态添加到 query 中。

一般而言,IFTTT 是一种在指定事件发生时触发操作的规则。在我们的场景中,事件与操作的对应关系如下:

  • 「如果在 query 中发现字段 X」→「将指令 Y 附加到字段 X」
  • 「如果在 query 中发现指令 Z」→「在指令 Z 前后执行指令 Y」

向模式动态添加 IFTTT 指令是一个递归过程:该指令本身也可以配置自己的一组 IFTTT 指令,这些指令同样会被添加到指令链中。

使用场景

在底层,Gato GraphQL 的客户端使用此机制来配置 GraphQL 模式。

例如,Access Control 允许我们选择要应用于操作、字段和指令的访问控制规则。正是通过 IFTTT,这些规则才得以应用于 GraphQL 模式中的相应元素。

访问控制条目

以下是一些常见的使用场景:

按字段定义缓存控制 max-age

为所有字段附加 @CacheControl 指令,并自定义 maxAge 参数的值:Posturl 字段设置为 1 年,title 字段设置为 1 小时。

配置访问控制

User 类型的 email 字段附加 @validateDoesLoggedInUserHaveAnyRole 指令,使只有管理员才能查询用户的电子邮件地址。

同步访问控制与缓存控制

通过链式连接指令,可以确保在验证用户是否可以访问某个字段或指令时,响应不会被缓存。例如:

  • me 字段附加 @validateIsUserLoggedIn 指令
  • @validateIsUserLoggedIn 指令附加 @CacheControl 指令,并将 maxAge 参数值设为 0

加强安全性

@translate 指令附加 @validateIsUserLoggedIn 指令,以防止恶意行为者向 GraphQL 服务执行 query,导致服务器宕机并产生高额费用(在此场景中,@translate 基于 Google Translate,使用该服务需要付费)。

工作原理

如何通过 IFTTT 向模式添加指令?举个例子,假设我们想创建一个自定义指令 @authorize(role: String!),用于验证执行字段 myPosts 的用户是否具有预期角色 author,否则显示错误。

如果使用 SDL 创建模式,它将如下所示:

directive @authorize(role: String!) on FIELD_DEFINITION
 
type User {
  myPosts: [Post] @authorize(role: "author")
}

IFTTT 规则定义了与上述 SDL 声明相同的意图:每当请求字段 myPosts 时,对其执行指令 @authorize(role: "author")。当在 query 中发现字段 myPosts 时,引擎会自动将 @authorize(role: 'author') 附加到可执行 query 中的该字段上。

IFTTT 规则也可以在遇到指令时触发,而不仅仅是字段。例如,可以设置规则「每当在 query 中发现指令 @translate 时,对该字段执行指令 @cache(time: 3600)」。

向 query 添加 IFTTT 指令是一个递归过程:它会触发新事件由 IFTTT 规则处理,可能向 query 附加其他指令,如此循环往复。

例如,规则「每当发现指令 @cache 时,执行指令 @log」会记录该字段执行的日志条目,并针对这个新添加的指令触发新事件。

通过 PHP 代码进行配置

User 类型有 rolescapabilities 字段,这些字段可能被视为敏感信息,因此不应允许普通用户访问。

我们可以为这两个字段附加 @validateDoesLoggedInUserHaveAnyRole 指令,配置为只有具有特定角色(通过环境变量配置)的用户才能访问它们。配置通过 CompilerPass 提供:

$accessControlManagerDefinition = $containerBuilderWrapper->getDefinition(AccessControlManagerInterface::class);
 
if ($roles = Environment::anyRoleLoggedInUserMustHaveToAccessRolesFields()) {
  $accessControlManagerDefinition->addMethodCall(
    'addEntriesForFields',
    [
      UserRolesAccessControlGroups::ROLES,
      [
        [RootObjectTypeResolver::class, 'roles', $roles],
        [UserObjectTypeResolver::class, 'roles', $roles],
        [RootObjectTypeResolver::class, 'capabilities', $roles],
        [UserObjectTypeResolver::class, 'capabilities', $roles],
      ]
    ]
  );
}
if ($capabilities = Environment::anyCapabilityLoggedInUserMustHaveToAccessRolesFields()) {
  $accessControlManagerDefinition->addMethodCall(
    'addEntriesForFields',
    [
      UserCapabilitiesAccessControlGroups::CAPABILITIES,
      [
        [RootObjectTypeResolver::class, 'roles', $capabilities],
        [UserObjectTypeResolver::class, 'roles', $capabilities],
        [RootObjectTypeResolver::class, 'capabilities', $capabilities],
        [UserObjectTypeResolver::class, 'capabilities', $capabilities],
      ]
    ]
  );
}

执行 query 时,未登录的用户以及不具备所需角色的用户将不被允许访问这些字段。