graphqlapollomiddlewareapollo-serverresolver

Graphql - universal permission guards


I am trying to implement permission guards in a graphql backend using apollo server. The following code works:

Resolvers

const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');

notification: combineResolvers(isNotificationOwner, async (_, { id }) => {
  try {
    const notification = await Notification.findById(id);
    return notification;
  } catch (error) {
    throw error;
  }
})

task: combineResolvers(isTaskOwner, async (_, { id }) => {
  try {
    const task = await Task.findById(id);
    return task;
  } catch (error) {
    throw error;
  }
})

Resolver Middleware (permission guards)

const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');

// userId is the id of the logged in user retrieved from the context
module.exports.isNotificationOwner = async (_, { id }, { userId }) => {
  try {
    const notification = await Notification.findById(id);
    if (notification.user.toString() !== userId) {
      throw new ForbiddenError('You are not the owner');
    }
    return skip;
  } catch (error) {
    throw error;
  }
}

module.exports.isTaskOwner = async (_, { id }, { userId }) => {
  try {
    const task = await Task.findById(id);
    if (task.user.toString() !== userId) {
      throw new ForbiddenError('You are not the owner');
    }
    return skip;
  } catch (error) {
    throw error;
  }
}

Going on like this will create a lot of duplicate code and does not feel very DRY. Therefore, I am trying to create a more universal solution, to no evail so far.



What I tried:


Resolvers

const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');

notification: combineResolvers(isOwner, async (_, { id }) => {
  try {
    const notification = await Notification.findById(id);
    return notification;
  } catch (error) {
    throw error;
  }
})

task: combineResolvers(isOwner, async (_, { id }) => {
  try {
    const task = await Task.findById(id);
    return task;
  } catch (error) {
    throw error;
  }
})

Resolver Middleware

const Notification = require('../../database/models/notifications');
const Task = require('../../database/models/tasks');

module.exports.isOwner = async (_, { id, collection }, { userId }) => {
  try {
    const document = await collection.findById(id);
    if (document.user.toString() !== userId) {
      throw new ForbiddenError('You are not the owner');
    }

    return skip;
  } catch (error) {
    throw error;
  }
}

I am unable to pass a collection name as an argument to the middleware resolver.

I would be tremendously thankful for any kind of help!


Solution

  • Based off your code, it seems like you're looking for making isOwner a higher-order function, so that you can pass in the collection, and it returns the curried method.

    module.exports.isOwner = (collection) => {
      return async (_, { id }, { userId }) => {
        try {
          const document = await collection.findById(id);
          if (document.user.toString() !== userId) {
            throw new ForbiddenError('You are not the owner');
          }
    
          return skip;
        } catch (error) {
          throw error;
        }
      }
    }
    

    Usage:

    const resolvers = {
      Query: {
        task: combineResolvers(isOwner(Task), async (_, { id }) => {
          try {
            const task = await Task.findById(id);
            return task;
          } catch (error) {
            throw error;
          }
        })
      },
    };