graphql-compose / graphql-compose-mongoose

Mongoose model converter to GraphQL types with resolvers for graphql-compose https://github.com/nodkz/graphql-compose
MIT License
708 stars 94 forks source link

Whitelist fields in queries #334

Open riggedCoinflip opened 3 years ago

riggedCoinflip commented 3 years ago

I have a user model like this:

//shortened schema
const UserSchema = new mongoose.Schema({
    name: {
        type: String,
    },
    email: {
        type: String,
    },
    password: { //hashed
        type: String,
    },
    role: {
        type: String,
    },
    favouriteColor: {
        type: String,
    }
}, {
    timestamps: true,
});

export const User = mongoose.model('User', UserSchema)

and I want to have different "views" for different permission roles: An admin has full access. A moderator can view all fields except password and email A basic user can only see the name and favouriteColor.

The way I am currently doing it (which works) is to create a different TC for every permission role:

export const UserTCAdmin = composeMongoose(User, {
    name: "UserAdmin",
    description: "Full User Model. Exposed only for Admins."
});

export const UserTCMod = composeMongoose(User, {
    name: "UserMod",
    description: "Hide login information",
    removeFields: [
        "email",
        "password"
    ]
});

export const UserTCPublic = composeMongoose(User, {
    name: "UserPublic",
    description: "Contains all public fields of users. Use this for filtering as well",
    onlyFields: [
        "name",
        "favouriteColor"
    ]
})

but I have the feeling this is a suboptimal solution. If I have a custom resolver that I want to reuse in different UserTCs I have to copy-paste the code.

I think it could be a good solution to whitelist fields resolver-based in the options. An example of how this could look like:

export const UserTC = composeMongoose(User, {
    name: "User",
    description: "One UserTC for all"
});

//resolvers

//example custom resolver
UserTCAdmin.addResolver({
    kind: "query",
    name: "random",
    description: "Get a random user",
    ...
})

const publicFields = {
    onlyFields: [
        "name",
        "favouriteColor"
    ]
}

const modFields = {
    removeFields: [
        "email",
        "password"
    ]
}

export const UserQuery = {
    ...requireAuthentication({
        userOnePublic: UserTC.mongooseResolvers.findOne({fields: publicFields}),
        usernameRandom: UserTC.getResolver("superSpecial", {fields: onlyFields: "name"} //allow users to only get the name
    }),
    ...requireAuthorization({
        userOneMod: UserTC.mongooseResolvers.findOne({fields: modFields}),
        },
        "mod"
    ),
    ...requireAuthorization({
        userOneAdmin: UserTC.mongooseResolvers.findOne(),
        userRandom: UserTC.getResolver("superSpecial",) //allow admins to get the full schema
        },
        "admin"
    ),
};

What do you think?

canac commented 3 years ago

I think that you can achieve this with a resolver wrapper. You can wrap your findOne resolver to run code that limits the visible fields based on the user's role, something like this:

UserTC.mongooseResolvers.findOne.wrapResolve((next) => (rp) => {
  const { role } = resolveParams.context;

  rp.beforeQuery = (query: Query<unknown, never>) => {
    if (role === 'admin') {
      // Don't change the projection and allow all fields
    } else if (role === 'moderator') {
      query.select({ email: 0, password: 0 });
    } else if (role === 'public') {
      query.projection({ name: 1, favouriteColor: 1 });
    }
  };
  return next(rp);
}),

You'll have to pass your role in as context. If you're using ApolloServer, there's a context method you can use to add context to each request, and I'm sure that other GraphQL servers have a similar way of doing it. Hope that points you in the right direction!