Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL request is not triggering the global guard #3024

Open
2 of 4 tasks
ErangaHeshan opened this issue Oct 6, 2023 · 1 comment
Open
2 of 4 tasks

GraphQL request is not triggering the global guard #3024

ErangaHeshan opened this issue Oct 6, 2023 · 1 comment

Comments

@ErangaHeshan
Copy link

ErangaHeshan commented Oct 6, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Current behavior

I have a user role guard that needs to access a TypeORM repository inside. I set it inside my AppModule as a global guard. However, I see that when a GraphQL request comes, it does not go through the guard but simply hits my GraphQL query. Any idea what is going wrong here?

AppModule

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'src/schema.gql',
      context: ({ req }) => ({
        req,
        user: new JwtService().decode(
          (req.headers.authorization as string).replace('Bearer ', ''),
        ),
      }),
    }),
    // I need to make `TypeOrmModule` asynchronous because not all the requests
    // will need a database connection, and the requests that need will carry
    // the information to initialize the database connection inside the
    // authorization header as a JWT.
    //
    // e.g.: Check the following payload inside the JWT.
    //
    // ```json
    // {
    //   "id": "2",
    //   "database": "test",
    //   "iat": 1696519539,
    //   "exp": 1696526739
    // }
    // ```
    //
    TypeOrmModule.forRootAsync({
      useClass: TypeOrmConfigService,
      dataSourceFactory: async (options) =>
        await TypeOrmConfigService.getOrCreateDataSource(options),
    }),
    UserDetailsModule,
  ],
  providers: [
    AppResolver,
    {
      provide: APP_GUARD,
      // I'm using the factory method here since the repository needs to change based
      // on the customer. We have a separate database for each customer. If I use
      // `useClass: UserRightGuard` syntax here, I get `undefined` for `reflector`.
      useFactory: (reflector, repo) => new UserRightGuard(reflector, repo),
      inject: [Reflector, getRepositoryToken(UserDetails)],
      // If I make this request scoped, the guard will execute but that is not
      // the solution I want since not all the requests (e.g.: login) will have
      // user info in the request header.
      //
      // scope: Scope.REQUEST,
    },
  ],
})
export class AppModule {}

UserRightGuard

@Injectable()
export class UserRightGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private userDetailsRepo: Repository<UserDetails>,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    try {
      const requiredRights = this.reflector.getAllAndOverride(RequireRights, [
        context.getHandler(),
        context.getClass(),
      ]);
      // Returns `true` if there is no right specified as required.
      if (!requiredRights) return true;

      const ctx = GqlExecutionContext.create(context).getContext();
      const { user } = ctx;
      const { right } = await this.userDetailsRepo.findOne({
        where: { id: user.id },
      });
      return requiredRights.some((r) => r == right);
    } catch (err) {
      return false;
    }
  }
}

UserDetailsResolver

@Resolver(() => UserDetails)
export class UserDetailsResolver {
  constructor(
    @InjectRepository(UserDetails)
    private userDetailsRepository: Repository<UserDetails>,
  ) {}

  @RequireRights(['subscription'])
  @Query(() => [UserDetails])
  public async userDetails(): Promise<UserDetails[]> {
    return await this.userDetailsRepository.find();
  }

  @Query(() => UserDetails)
  public async firstUserDetail(): Promise<UserDetails> {
    const userDetails = await this.userDetailsRepository.find();
    return userDetails[0];
  }
}

Minimum reproduction code

https://github.com/ErangaHeshan/nests-issues-trigger-global-guard#2-a-user-without-proper-access

Steps to reproduce

No response

Expected behavior

I would expect the GraphQL queries annotated using @RequireRights(['subscription']) to invoke the guard and check if the user who made the request has appropriate right value in their user_details table. If the user does not have proper right, the query should return FORBIDDEN error inside its response body.

Package version

12.0.8

Graphql version

graphql: 16.8.1
@apollo/server: 4.9.4
@nestjs/platform-express: 10.0.0
@nestjs/typeorm: 10.0.0

NestJS version

10.0.0

Node.js version

16.13.2

In which operating systems have you tested?

  • macOS
  • Windows
  • Linux

Other

I discussed the issue with one of the NestJS core team member and here is our discussion on Discord

@lucassith
Copy link

lucassith commented Jan 24, 2024

I'm experiencing the same problem. I have registered GraphQLModule.forRootAsync and I also have simple controller with single /health GET request.

My guard is simple, just logging on canActivate. I set it as global guard with: app.useGlobalGuards(new TestGuard());

GraphQL routes simply ignore the guard meanwhile /health request prints logs.

"@nestjs/apollo": "^12.0.11",
"@neo4j/graphql": "^4.4.5",
"@apollo/server": "^4.9.4",
"@nestjs/core": "^10.3.1",
"@nestjs/graphql": "^12.0.9",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants