アーキテクチャ
アーキテクチャディレクティブによるIFTTT

ディレクティブによるIFTTT

Gato GraphQLは、ディレクティブを通じてIFTTT(If This Then That)戦略を実装する機能を提供しています。これらのディレクティブは、クエリ内に特定のフィールドまたはディレクティブが存在する場合に、動的にクエリへ追加されます。

一般的に、IFTTTとは指定されたイベントが発生するたびにアクションを起動するルールです。このケースでは、イベントとアクションのペアは次のとおりです。

  • 「クエリにフィールドXが見つかった場合」→「フィールドXにディレクティブYを付加する」
  • 「クエリにディレクティブ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ディレクティブに、maxAge引数の値を0として@CacheControlディレクティブを付加する

セキュリティの強化

@translateディレクティブに@validateIsUserLoggedInディレクティブを付加することで、悪意のある者がGraphQLサービスに対してクエリを実行し、サーバーをダウンさせたり料金を急増させたりするのを防ぎます(この場合、@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")を実行します。クエリ内でフィールドmyPostsが見つかると、エンジンは実行可能なクエリのそのフィールドに自動的に@authorize(role: 'author')を付加します。

IFTTTルールは、フィールドだけでなくディレクティブに遭遇した際にも起動できます。たとえば、「クエリ内にディレクティブ@translateが見つかった場合、そのフィールドに対してディレクティブ@cache(time: 3600)を実行する」というルールを設定できます。

クエリへのIFTTTディレクティブの追加は再帰的なプロセスです。新しいイベントが起動してIFTTTルールによって処理され、さらに他のディレクティブがクエリに付加される可能性があり、このプロセスが続きます。

たとえば、「ディレクティブ@cacheが見つかった場合、ディレクティブ@logを実行する」というルールは、フィールドの実行に関するエントリーを記録し、さらにこの新しく追加されたディレクティブに関する新しいイベントを起動します。

PHPコードによる設定

Userタイプにはrolescapabilitiesというフィールドがあり、これらは機密情報とみなされる可能性があるため、一般のユーザーはアクセスできないようにするべきです。

そのため、これら2つのフィールドに@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],
      ]
    ]
  );
}

クエリを実行する際、ログインしていないユーザーや必要なロールを持たないユーザーは、これらのフィールドへのアクセスが許可されません。