Whitelist fields in queries
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?
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!