Implement field permissions with FieldMiddleware
choco
Posted on January 21, 2023
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
Also use Express and Apollo, following Quick start.
yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
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 {}
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)
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;
}
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)
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',
};
}
}
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 {}
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;
};
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;
}
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"}}}
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
Posted on January 21, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.