TriPSs / nestjs-query

Easy CRUD for GraphQL.
https://tripss.github.io/nestjs-query/
MIT License
152 stars 43 forks source link

Improve subscriptions with authorize filter #284

Open jakubnavratil opened 1 month ago

jakubnavratil commented 1 month ago

Problem Using DTO authorizer, I'm able to query filtered collections based on user role. Role Admin sees everything but role User sees only his own records. Now when both subscribe to updates and Admin updates record, User does NOT get notified and vice versa, because they have different authorize filters.

Original solutions for that was done in https://github.com/TriPSs/nestjs-query/commit/d2f857f73540ee400f5dcc79cbb25dfba81c2963 It introduced separate PubSub event using original eventName and authorizeFilter https://github.com/TriPSs/nestjs-query/blob/1568cfde3652d0ebf2fc1c7e6e671a047d06e517/packages/query-graphql/src/resolvers/helpers.ts#L39

So Admin will publish event eventName-A on update and is subscribed to the same event, but User will publish and subscribe to eventName-B. That way implementation doesn't leak data to user without proper role BUT not everyone gets notified even if they should.

Solution This can be solved by using existing applyFilter on subscription iterator inside subscription resolver method.

How this could look? Assume this helper exists:

async function* filterAsync<T>(
  iterator: AsyncIterator<T>,
  predicate: (item: T) => boolean | Promise<boolean>,
): AsyncIterator<T> {
  while (true) {
    const { value, done } = await iterator.next();
    if (done) break;

    if (await predicate(value)) {
      yield value;
    }
  }
}

Now we can filter results from pubSub.asyncIterator(eventName). This is how implementation of updateOneSubscription could look:

updatedOneSubscription(
  @Args({ type: () => UOSA }) input?: any,
  @AuthorizerFilter({ operationGroup: OperationGroup.UPDATE, many: false })
  authorizeFilter?: Filter<DTO>,
): AsyncIterator<UpdatedEvent<DTO>> {
  if (!enableOneSubscriptions || !this.pubSub) {
    throw new Error(`Unable to subscribe to ${updateOneEvent}`);
  }
  const iter = this.pubSub.asyncIterator<UpdatedEvent<DTO>>(updateOneEvent);
  if (authorizeFilter == null) {
    return iter;
  }

  return filterAsync(iter, (payload) =>
    applyFilter(payload[updateOneEvent], authorizeFilter),
  );
}

async publishUpdatedOneEvent(
  dto: DTO,
  authorizeFilter?: Filter<DTO>,
): Promise<void> {
  if (this.pubSub) {
    await this.pubSub.publish(updateOneEvent, { [updateOneEvent]: dto });
  }
}

My current solution I'm currently monkey-patching these methods with custom decorator (cause overriding all methods with proper types is just too much, I'm already overriding some).

Would you be interested in PR? It would very much helped me, if this could be included in library. From my perspective, this is how subscription should work. I'll be willing to make PR if there is more interest in this. But my hope is it would be merged if possible, not forgotten :)