Implement field permissions with FieldMiddleware

choco14t

choco

Posted on January 21, 2023

Implement field permissions with FieldMiddleware

Introduction

You may be implementing an API in GraphQL and want to apply access control to a field of a certain type. For example, logged-in users can access a field, or only users with certain privileges can access a field, and so on.

In NestJS, such fields can be controlled using FieldMiddleware. This article explains how to create a NestJS project and apply FieldMiddleware.

The code created in this article can be found in the following repository.

https://github.com/choco14t/field-middleware-example

About FieldMiddleware

FieldMiddleware, as the name suggests, provides functions for adding processing before and after fields are resolved.

FieldMiddleware is defined by a function that takes two arguments of type MiddlewareContext and NextFn. The MiddlewareContext type is defined by Nest, but the value passed is the same as the value received by the GraphQL Resolver ({ source, args, context, info }). See official GraphQL documentation for more details on values.

Note that FieldMiddleware does not have access to the DI container. If you want to execute an external API or access a DB, you need to make it accessible from MiddlewareContext.

Example implementation with context

The following is an example of implementation.

  • Define a Query.user that can retrieve a single user from an id.
  • The user has the fields id, name, and email.
  • Only users with administrative privileges can access email. If any other user tries to retrieve it, null is returned.

In this case, we will create a project using the following.

  • @nestjs/cli 8.2.6
  • yarn 1.22.19
  • Node.js 16.15.0
nest new field-middleware-example
Enter fullscreen mode Exit fullscreen mode

Also use Express and Apollo, following Quick start.

yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
Enter fullscreen mode Exit fullscreen mode

Importing a GraphQLModule

First, import the GraphQLModule into the AppModule.

import { join } from 'path';
import { ApolloDriverConfig, ApolloDriver } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
    }),
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Create UserModule

If you try to launch the application at this point, you will get an error message that no Query or Mutation has been defined. First, we will create a UserModule so that we can start the application.

nest generate module user
# output
CREATE src/user/user.module.ts (81 bytes)
UPDATE src/app.module.ts (478 bytes)
Enter fullscreen mode Exit fullscreen mode

Next, define the Type. In this case, we define an email with limited access and a name that is always accessible. At this point, both email and name are always accessible.

import { Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field({ nullable: true })
  email: string;
}
Enter fullscreen mode Exit fullscreen mode

Next, create a Resolver.

nest generate resolver user
# output
CREATE src/user/user.resolver.spec.ts (456 bytes)
CREATE src/user/user.resolver.ts (86 bytes)
UPDATE src/user/user.module.ts (158 bytes)
Enter fullscreen mode Exit fullscreen mode

In the article, we only need to check the value, so we define a Query.user that returns a fixed value.

import { Query, Resolver } from '@nestjs/graphql';
import { User } from './user.type';

@Resolver(() => User)
export class UserResolver {
  @Query(() => User)
  user() {
    return {
      id: '1',
      name: 'user 1',
      email: 'user_1@example.com',
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the application is successfully started and the query can be executed.

Setting up the context

Next, we set up a context to hold the logged-in user. context can be defined in a GraphQLModule.

As a side note, if the context is undefined, the context will be set to an object predefined in the Apollo package you are using.

NestJS defaults to one of the following:

framework context
Express { req: Express.Request, res: Express.Response }
Fastify { request: FastifyRequest, reply: FastifyReply }

This time, we will check the Authorization header of the request to determine the user accessing it. Type and dummy data should be written directly in the AppModule at first.

 import { join } from 'path';
 import { ApolloDriverConfig, ApolloDriver } from '@nestjs/apollo';
 import { Module } from '@nestjs/common';
 import { GraphQLModule } from '@nestjs/graphql';

+ enum ViewerRole {
+   MEMBER,
+   ADMIN,
+ }
+
+ interface Viewer {
+   userName: string;
+   role: ViewerRole;
+ }
+
+ interface AppContext {
+   viewer: Viewer | undefined;
+ }
+
+ const users: Viewer[] = [
+   { userName: 'user_1', role: ViewerRole.MEMBER },
+   { userName: 'user_2', role: ViewerRole.ADMIN },
+ ];

 @Module({
   imports: [
     GraphQLModule.forRoot<ApolloDriverConfig>({
       driver: ApolloDriver,
       autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
       sortSchema: true,
+      context: ({ req }): AppContext => {
+        const token = req.headers.authorization || '';
+        const viewer = users.find((user) => user.userName === token);
+
+        return { viewer };
+      },
     }),
     UserModule,
   ],
 })
 export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

FieldMiddleware implementation

Implement hasAdminRole to determine if the user has administrative privileges.

FieldMiddleware is a Generics of FieldMiddleware<TSource, TContext, TArgs, TOutput>, and each argument corresponds to the following type of information.

Arguments Corresponding type information
TSource ObjectType of the passed field.
TContext The context defined by the GraphQL server, which in this article is AppContext.
TArgs Arguments used for the field. If there are no arguments, they are {}.
TOutput The value returned by the FieldMiddleware.

In light of the above arguments, the implementation is as follows.

import { FieldMiddleware } from '@nestjs/graphql';
import { AppContext, ViewerRole } from '... /... /app.module';
import { User } from '... /user.type';

export const hasAdminRole: FieldMiddleware<User, AppContext> = async (
  ctx,
  next,
) => {
  const {
    context: { viewer }
  } = ctx;

  return viewer?.role === ViewerRole.ADMIN ? next() : null;
};
Enter fullscreen mode Exit fullscreen mode

There is no reference to TSource in hasAdminRole, but it is explicit because it is Middleware used for User.

Applying FieldMiddleware

Apply the FieldMiddleware implemented in the previous section to User.email. This works by passing an array of FieldMiddleware to the middleware property of FieldOptions.

 import { Field, ID, ObjectType } from '@nestjs/graphql';
 import { hasAdminRole } from './field-middlewares/check-admin-role.middleware';

 @ObjectType()
 export class User {
   @Field(() => ID)
   id: string;

   @Field()
   name: string;

+  @Field({ nullable: true, middleware: [hasAdminRole] })
   email: string;
 }
Enter fullscreen mode Exit fullscreen mode

Execute Query

Finally, check that the FieldMiddleware works.

## Request by a user with non-administrative privileges
curl -H 'Content-Type: application/json' -H 'Authorization: user_1' -d '{ "query": "query { user { email } }" }' http://localhost:3000/graphql
# output
{"data":{"user":{"email":null}}

# Request as user with admin rights
curl -H 'Content-Type: application/json' -H 'Authorization: user_2' -d '{ "query": "query { user { email } }" }' http://localhost:3000/graphql
# output
{"data":{"user":{"email": "user_1@example.com"}}}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The FieldMiddleware can be applied globally to the entire application, so if you are interested, please give it a try.

NestJS provides useful features for Code First implementations. However, compared to graphql-shield, FieldMiddleware does not provide the ability to determine if one of the rules such as or is satisfied. Therefore, if you want to apply complex rules, FieldMiddleware may not be able to do so.

It is possible to use graphql-shield on NestJS, so it would be better to use different libraries depending on your project.

References

💖 💪 🙅 🚩
choco14t
choco

Posted on January 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related