⚙️ Mandatory directives for fields/directives in GraphQL by PoP

— 5 minute read

I just added support for a lovely new feature in GraphQL by PoP: the ability to execute mandatory directives whenever a specific field or directive is present in the GraphQL query. This means that the GraphQL engine will:

  • Whenever a specific field from some type is present in the query, add a certain directive (or directives) to be executed on the field
  • Whenever a specific directive is invoked, execute another directive (or directives) before it

A mandatory directive can, itself, have its own set of mandatory directives which are also added to the directive chain up.

This feature is extremely powerful, since it allows to easily configure what directives are added to the query under what circumstances, as to implement any IFTTT strategy. It supports adding the following capabilities to our GraphQL API:

Define the cache control max-age a field by field basis

Attach a @CacheControl directive to all fields, customizing the value of the maxAge parameter: 1 year for the Post's field url, and 1 hour for field title.

Set-up access control

Attach a @validateDoesLoggedInUserHaveAnyRole directive to field email from the User type, so only the admins can query the user email.

Synchronize access-control with cache-control

By chaining up directives, we can make sure that, whenever validating if the user can access a field/directive, the response will not be cached. For instance:

  • Attach directive @validateIsUserLoggedIn to field @me
  • Attach directive @CacheControl with maxAge argument value of 0 to directive @validateIsUserLoggedIn.

Beef up security

Attach a @validateIsUserLoggedIn directive to directive @translate, to avoid malicious actors executing queries against the GraphQL service that can bring the server down and spike its bills (in this case, @translate is based on Google Translate and it pays a fee to use this service)

Demostration permalink

In this schema, the User type has fields roles and capabilities, which I consider to be sensitive information, so it should not be accessible by the random user.

Then, I created package Access Control List for User Roles to attach directive @validateDoesLoggedInUserHaveAnyRole to these two fields, configured to validate that only a user with a given role can access them (code here):

if ($roles = Environment::anyRoleLoggedInUserMustHaveToAccessRolesFields()) {
ContainerBuilderUtils::injectValuesIntoService(
'access_control_manager',
'addEntriesForFields',
UserRolesAccessControlGroups::ROLES,
[
[RootTypeResolver::class, 'roles', $roles],
[SiteTypeResolver::class, 'roles', $roles],
[UserTypeResolver::class, 'roles', $roles],
[UserTypeResolver::class, 'capabilities', $roles],
]
);
}

When executing the query, dear reader, you won't be allowed to access those fields, since you are not logged in (which is validated before checking if the user has the required role):