nestjs-query: Investigate potential bug in how Authorizers of DTOs are resolved

Describe the bug

authorize of DTOs involved in a query are not called in all cases, potentially leaking data to unauthorized users.

Have you read the Contributing Guidelines?

yes

To Reproduce Steps to reproduce the behavior:

  1. I have prepared a private repo with a working example (derived from the examples/auth of this repo). It would be awesome if some of the core devs or anyone else who’s willing to take some time and investigate/discuss the problem with me. I’d then invite you to my private project and show you my prepared example/pull request and step-by-step introductions on how to reproduce the problem.
  2. I’d like to discuss the issue/potential bug there first before disclosing the behavior publicly (to prevent any abuse of the behavior).
  3. I’ve already reached out to @doug-martin via email but did not get a response yet (I assume the email has gone to the spam folder or Doug just didn’t get to it yet - we’re all busy and bombarded with notifications. So, no offense taken here).
  4. I’ve prepared a potential fix, but don’t know the code base well enough to know if that’s the correct solution.

Expected behavior authorize of DTOs involved in a query are called in all cases

Screenshots Will be available in my private repository/pull request

Desktop (please complete the following information):

  • Node Version: v14.17.6
  • Nestjs-query Version 88d744a7eb7f6b516d4914fee4cedd4773de82a4

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 2
  • Comments: 18 (11 by maintainers)

Most upvoted comments

@steinroe I agree option 2 sounds like the best path to me and would lead to a better end-user experience where “it just works” without additional configuration that could be easily missed. Thank you both for figuring this out and contributing back!

FYI: I struggled several days with the framework not applying authorization as expected on a custom resolvers. Eventually, the following line from this comment https://github.com/doug-martin/nestjs-query/issues/1344#issuecomment-936862663 solved it for me.

GraphQLModule.forRoot({
    //...
    fieldResolverEnhancers: ['interceptors'],
}),

Now authorization is applied as I expected it to.

I still need to understand the consequences of enabling field level enhancers to say whether that’s an okay workaround for me or not. Nestjs docs give some input https://docs.nestjs.com/graphql/other-features#execute-enhancers-at-the-field-resolver-level

Alright I know what the issue is. The interceptor is applied on field level

@Resolver(() => DTOClass, { isAbstract: true })
    class ReadManyMixin extends Base {
      @ResolverField(
        pluralBaseNameLower,
        () => CT.resolveType,
        { nullable: relation.nullable, complexity: relation.complexity },
        commonResolverOpts,
        { interceptors: [AuthorizerInterceptor(DTOClass)] }, // <-- HERE
      )
      async [`query${pluralBaseName}`](
        @Parent() dto: DTO,
        @Args() q: RelationQA,
        @Context() context: ExecutionContext,
        @RelationAuthorizerFilter(pluralBaseNameLower, {
          operationGroup: OperationGroup.READ,
          many: true,
        })
        relationFilter?: Filter<Relation>,
      ): Promise<InstanceType<typeof CT>> {

and to run enhancers on field level, fieldResolverEnhancers: ['interceptors'], has to be set in the GQL Server options.

With that option, the tests pass.

However, this then leads to the behaviour I explained above: Intercept for resource A --> ResourceA Authorizer.authorize()--> Intercept for Resource B --> Resource B Authorizer.authorize()--> Intercept for resource C --> Resource C Authorizer.authorize() Hence, we loose the ability to overwrite the relation authorisation. For each DTO, only the authorize() of the respective DTO is used once we enable interceptors on field level.

I see the following options:

  • Drop the ability to overwrite relation authorisation and force folks to enable field level interceptors
  • Basically Option 2 from above: Try to get all relation authorisers from within the first authorizer that is called. There might be some challenges in the details of the implementation, but it should be possible.

I guess this is something only @doug-martin can decide. I am happy to implement it though.

Awesome @lukasender, thanks! I will dig into it this week.