How to check permissions and other conditions in GraphQL query? - node.js

How could I check if user has permission to see or query something? I have no idea how to do this.
In args? How would that even work?
In resolve()? See if user has permission and somehow
eliminate/change some of the args?
Example:
If user is "visitor", he can only see public posts, "admin" can see everything.
const userRole = 'admin'; // Let's say this could be "admin" or "visitor"
const Query = new GraphQLObjectType({
name: 'Query',
fields: () => {
return {
posts: {
type: new GraphQLList(Post),
args: {
id: {
type: GraphQLString
},
title: {
type: GraphQLString
},
content: {
type: GraphQLString
},
status: {
type: GraphQLInt // 0 means "private", 1 means "public"
},
},
// MongoDB / Mongoose magic happens here
resolve(root, args) {
return PostModel.find(args).exec()
}
}
}
}
})
Update - Mongoose model looks something like this:
import mongoose from 'mongoose'
const postSchema = new mongoose.Schema({
title: {
type: String
},
content: {
type: String
},
author: {
type: mongoose.Schema.Types.ObjectId, // From user model/collection
ref: 'User'
},
date: {
type: Date,
default: Date.now
},
status: {
type: Number,
default: 0 // 0 -> "private", 1 -> "public"
},
})
export default mongoose.model('Post', postSchema)

You can check a user's permission in the resolve function or in the model layer. Here are the steps you have to take:
Authenticate the user before executing the query. This is up to your server and usually happens outside of graphql, for example by looking at the cookie that was sent along with the request. See this Medium post for more details on how to do this using Passport.js.
Add the authenticated user object or user id to the context. In express-graphql you can do it via the context argument:
app.use('/graphql', (req, res) => {
graphqlHTTP({ schema: Schema, context: { user: req.user } })(req, res);
}
Use the context inside the resolve function like this:
resolve(parent, args, context){
if(!context.user.isAdmin){
args.isPublic = true;
}
return PostModel.find(args).exec();
}
You can do authorization checks directly in resolve functions, but if you have a model layer, I strongly recommend implementing it there by passing the user object to the model layer. That way your code will be more modular, easier to reuse and you don't have to worry about forgetting some checks in a resolver somewhere.
For more background on authorization, check out this post (also written by myself):
Auth in GraphQL - part 2

One approach that has helped us solve authorization at our company is to think about resolvers as a composition of middleware. The above example is great but it will become unruly at scale especially as your authorization mechanisms get more advanced.
An example of a resolver as a composition of middleware might look something like this:
type ResolverMiddlewareFn =
(fn: GraphQLFieldResolver) => GraphQLFieldResolver;
A ResolverMiddlewareFn is a function that takes a GraphQLFieldResolver and and returns a GraphQLFieldResolver.
To compose our resolver middleware functions we will use (you guessed it) the compose function! Here is an example of compose implemented in javascript, but you can also find compose functions in ramda and other functional libraries. Compose lets us combine simple functions to make more complicated functions.
Going back to the GraphQL permissions problem lets look at a simple example.
Say that we want to log the resolver, authorize the user, and then run the meat and potatoes. Compose lets us combine these three pieces such that we can easily test and re-use them across our application.
const traceResolve =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
const start = new Date().getTime();
const result = await fn(obj, args, context, info);
const end = new Date().getTime();
console.log(`Resolver took ${end - start} ms`);
return result;
};
const isAdminAuthorized =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
if (!context.user.isAdmin) {
throw new Error('User lacks admin authorization.');
}
return await fn(obj, args, context, info);
}
const getPost = (obj: any, args: any, context: any, info: any) => {
return PostModel.find(args).exec();
}
const getUser = (obj: any, args: any, context: any, info: any) => {
return UserModel.find(args).exec();
}
// You can then define field resolve functions like this:
postResolver: compose(traceResolve, isAdminAuthorized)(getPost)
// And then others like this:
userResolver: compose(traceResolve, isAdminAuthorized)(getUser)

Related

avoid calling a parent resolver if only nested resolver was called

lets say I have a simple query to get post's comments and it looks like this
post(id:"123") {
comments: {
id,
body
}
}
currently it the graph will call postResolver and then commentsResolver
but the call to postResolver is redundant since I only need to fetch all the comments by postId
I am using an implementation using nodeJs with typescript
i have a resolver such as this
const resolvers : Resolvers = {
Query: {
post: (parent, args, info) => { return fetchPost(args.id);}
},
Post: {
comments: (parent, args, info) => { return fetchComments(parent.id)}
}
}
basically in this example I don't need to fetch the post at all, but the resolver is still invoked, any way to elegantly avoid it ?
I'm looking of a generalized pattern and not this specific resolver situation, there are other nodes with same situation would like to know if there is anything common in this situation that was already solved ...
My solution so far is to remodel the graph like this
type Post (id: ID!){
postData: PostData,
comments: [Comment!]
}
type PostData {
id: ID! ...
}
type Comment{
id: ID! ....
}
Your original model is fine, you just need a different query that goes straight for the comments based on their postId:
getCommentsByPostId(postId: ID!): [Comment]
Then augment your query resolvers:
const resolvers : Resolvers = {
Query: {
post: (_, { id }) => { return fetchPost(id);},
getCommentsByPostId: (_, { postId }) => fetchComments(postId)
},
…

how to stop users from viewing and updating another user's data in node.js?

I am storing a parking detail with a merchant id in the mongoose schema since a parking belongs to a certain merchant user and it cannot be empty or null.
Here is the model:
const parkingSchema = new mongoose.Schema({
merchantId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "Merchant",
},
//other details
})
merchant model is something like this:
const merchantSchema = new mongoose.Schema({
merchantId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Auth",
},
//other details
})
And finally the auth schema:
const authSchema = new mongoose.Schema({
accountType: {
type: String,
required: true,
trim: true,
default: "user",
enum: ["merchant", "user", "provider"],
},
//other details
})
If the original user wishes it, I simply want to update the parking data; otherwise, I want to throw an error.
I am using jsonwebtoken to authenticate users.
Here is the query to update the data:
exports.updateParking = async (req, res) => {
try {
const { parkingName, price, address, name, phoneNumber, about } = req.body;
const { parkingImage } = req.files;
const check_exist = await Auth.findById(req.data.id);
if (!check_exist) return res.status(404).json({ error: "User not found" });
console.log(req.data.id);
const updateData = await Parking.findByIdAndUpdate(
{ _id: req.params.id, merchantId: req.data.id }, // I think here is the problem
{
$set: {
parkingName,
price,
address,
...
},
}
);
return res.status(200).json({
success: true,
msg: "Parking has updated successfully",
});
} catch (error) {
return error.message;
}
};
However, the issue is that other users can now update another user's data which I want to stop
below is the query of middleware:
routing.patch("/parking/update/:id", middleware.authenticateToken, merchant.updateParking)
You should be showing each user only their parkings that they have created or belong to them.
const myParkings = async (req, res) => {
// always await code in try/catch block
const merchants = await Parkings.find({ user: req.user._id })
.. then populate the fields that you want to show
res.status(200).json({
success: true,
bookings,
});
};
you have to set this req.user._id when user logins. You could create a session.
I think what you're looking for is something like CASL Mongoose (or a similar package), and more specifically, the "conditions" section of the CASL docs.
What you're dealing with here is the distinction between 2 concepts:
AuthN (authentication) - determines who someone is and whether they are "authenticated" to make an API request
AuthZ (authorization) - determines what the authenticated user is allowed to do
In your app, middleware.authenticateToken is responsible for the AuthN piece of the equation. It makes sure that only users that have created an account are able to make requests to your API routes.
What you still need to solve for is the AuthZ piece, which can be done in a bunch of different ways, but one popular one is to use CASL, which is a Node AuthZ library that allows you to utilize your ORM's native query syntax to limit actions based on the authenticated (AuthN) user's attributes.
In other words, you can do something like, "Only allow user with ID 1 to update Parking entities that he/she owns". Below is generally what you're looking for (not tested for your use case, but the general idea is here):
const casl = require('#casl/ability');
// Define what a `Auth` (user) can do based on their database ID
function defineMerchantAbilities(merchantUser) {
const abilities = casl.defineAbility((allow, deny) => {
// Allow merchant to update a parking record that they own
allow('update', 'Parking', { merchantId: merchantUser.id })
})
return abilities
}
exports.updateParking = async (req, res) => {
const userId = req.data.id
const parkingId = req.params.id
// Find your merchant user in DB (see my comments at end of post)
const merchantUser = await Auth.findById(userId)
// Find your parking record
const parking = await Parking.findById(parkingId)
// Pass user to your ability function
const ability = defineMerchantAbilities(merchantUser)
// This will throw an error if a user who does not own this Parking record
// tries to update it
casl.ForbiddenError
.from(ability)
.throwUnlessCan('update', casl.subject('Parking', parking))
// If you make it here, you know this user is authorized to make the change
Parking.findByIdAndUpdate( ...your code here )
}
Additional comments/notes:
I would recommend removing your try/catch handler and using an Express default error handler as it will reduce the boilerplate you have to write for each route.
I would also recommend writing a middleware that finds a user by ID in the database and attaches it to a custom property called req.user so you always have req.user available to you in your authenticated routes.

Rolling back resolve function from GraphQLObjectType to another

I'm currently studying GraphQL and as part of the developing process, i'm interested with modularization of my code - i do understand how to write query, but fail to understand how to correctly implement query of queries.
That is the rootQuery.js
const {
GraphQLInt,
GraphQLList,
GraphQLObjectType,
GraphQLSchema,
GraphQLFloat,
GraphQLString
} = require("graphql");
const bankRootQuery = require('../graphql/queries/bank.queries')
const rootQuery = new GraphQLObjectType({
name: "rootQuery",
fields: {
bankRootQuery: { type: bankRootQuery, resolve: () => { console.log(bankRootQuery.resolve) } }
}
});
module.exports = new GraphQLSchema({
query: rootQuery
});
And here is the bankRootQuery.js:
const { GraphQLObjectType, GraphQLInt, GraphQLNonNull, GraphQLID, GraphQLList } = require("graphql");
const BankType = require('../types/bank.type');
const models = require('../../models/models_handler');
module.exports = new GraphQLObjectType({
name: "bankRootQuery",
fields: {
getbanks: {
type: new GraphQLList(BankType),
resolve: () => {
return models.getBanks()
}
},
getbankByID: {
type: BankType,
args: {
bankID: { name: "bankID", type: GraphQLInt }
},
resolve: (_, args) => {
if (!models.getBanks().has(args.bankID))
throw new Error(`Bank with ID ${args.bankID} doesn't exists`);
return models.getBank(args.bankID);}
}
}
});
Assining bankRootQuery to the scheme object instead of rootQuery works perfectly fine, but using the rootQuery yields with null result when querying using GraphiQL - The Documentation Explorer structure seems to be in proper manner, so i'm guessing the problem is with the resolve function, which i don't understand how to define correctly.
Here is the result when querying using GraphQL:
{
"data": {
"bankRootQuery": null
}
}
If a field resolves to null, then execution for that "branch" of the graph ends. Even if the field's type is an object type, none of the resolvers for its "children" fields will be called. Imagine if you had a field like user -- if the field resolves to null, then it makes no sense to try to resolve the user's name or email.
Your resolver for the bankRootQuery field just logs to the console. Because it doesn't have a return statement, its return value is undefined. A value of undefined is coerced into a null. Since the field resolved to null, execution halts.
If you want to return something other than null, then your resolver needs to return something -- even if it's just an empty object ({}). Then the resolvers for any "child" fields will work as expected.
In general, I would advise against nesting your queries like this -- just keep them at the root level. For additional details around how field resolution works, check out this post.

Merging GraphQL Resolvers for Apollo Server not working with Object.assign()

I am modularizing my schema for a GraphQL API and trying to merge the resolvers without using any 3rd party libraries.
Is there a simple way to do this without Lodash.merge() or equivalent?
The Apollo Documentation says to use a library such as Lodash to merge() modularized resolvers. (http://dev.apollodata.com/tools/graphql-tools/generate-schema.html#modularizing)
The problem seems to be that by their nature, the resolvers contain functions as properties, so they seem to be omitted when I access them via Object.assign() or even JSON.stringify().
If I console.log them, I see: {"Query":{},"Mutation":{}}
Here is what one of the resolvers looks like:
const productResolvers = {
Query: {
myProducts: (root, { userId }, context) => {
return [
{ id: 1, amount: 100, expiry: '12625383984343', created: '12625383984343' },
{ id: 2, amount: 200, expiry: '12561351347311', created: '12625383984343' },
{ id: 3, amount: 200, expiry: '11346347378333', created: '12625383984343' },
{ id: 4, amount: 350, expiry: '23456234523453', created: '12625383984343' },
];
},
},
Mutation: {
addProduct: (root, { userId }, context) => {
return { id: 350, amount: 100, expiry: '12625383984343', created: '12625383984343' };
},
}
};
Let's assume there is another one virtually identical called widgetResolvers.
Here is a fully functional block of code:
export const schema = makeExecutableSchema({
typeDefs: [queries, mutations, productSchema, widgetSchema],
resolvers
});
Here is what I'm trying to achieve:
export const schema = makeExecutableSchema({
typeDefs: [queries, mutations, productSchema, widgetSchema],
resolvers: Object.assign({}, productResolvers, widgetResolvers)
});
I haven't loaded in ability to use rest spread yet (https://babeljs.io/docs/plugins/transform-object-rest-spread/). I suspect it won't work for the same reason Object.assign() doesn't work.
Oh, and here is why I suspect this merge doesn't work: Why doesn't JSON.stringify display object properties that are functions?
If you're using Object.assign(), your Query and Mutation properties shouldn't end up empty, but you will run into an issue because, unlike lodash's merge(), it's not recursive. Object.assign() only compares the "direct" properties of the objects it's passed -- overriding properties of previous sources as it moves through the list.
Because Query and Mutation are properties of the objects being passed, each subsequent resolver override the previous object's Query and Mutation, with the resulting object only holding the Query and Mutation properties of the last object passed into Object.assign().
It's a lot less neat, but if you're bent on avoiding importing lodash, you could get the expected behavior this way:
const productResolver = {
Query: { ... ✂ ... },
Mutation: { ... ✂ ... }
}
const widgetResolver = {
Query: { ... ✂ ... },
Mutation: { ... ✂ ... }
}
const resolvers = {
Query: Object.assign({}, widgetResolver.Query, productResolver.Query),
Mutation: Object.assign({}, widgetResolver.Mutation, productResolver.Mutation)
}
Got type resolvers too? No problem:
const Widget = { ... ✂ ... }
const Product = { ... ✂ ... }
const resolvers = Object.assign(
{
Query: Object.assign({}, widgetResolver.Query, productResolver.Query),
Mutation: Object.assign({}, widgetResolver.Mutation, productResolver.Mutation)
},
Widget,
Product)

How to implement reference to another model in Relay Mutations on the server side?

I'm stuck with Refs in Relay Mutation and globalIdField.
So let's say that comment can have parent comment id, must have post id in props and I have a Mutation defined with following schema:
const createComment = mutationWithClientMutationId({
name: 'CreateComment',
description: 'Create comment of the post',
inputFields: {
content: {
type: new GraphQLNonNull(GraphQLString),
description: 'Content of the comment',
},
parent: {
type: GraphQLID,
description: 'Parent of the comment',
},
post: {
type: new GraphQLNonNull(GraphQLID),
description: 'Post of the comment',
},
},
outputFields: {
comment: { type: commentType, resolve: comment => comment },
},
mutateAndGetPayload: (input, context) => (context.user
? Comment.create(Object.assign(input, { author: context.user._id }))
: new Error('Only logged in user can create new comment')),
});
My comment has globalIdField, postType too. When I will query mutation from client I will use everywhere globalIds instead of real mongo _id of this objects. Is here the better way for it instead this piece in mutateAndGetPayload:
mutateAndGetPayload: (input, context) => {
if (input.parent) input.parent = fromGlobalId(input.parent).id;
if (input.post) input.post = fromGlobalId(input.post).id;
// And other logic
}
It can be very convinient if I can just add globalIdField() in post, but Relay can't pass this, because field in inputFields can't have a resolver function which globalIdField has.
So far I can't find solution better than below:
mutateAndGetPayload: ({ content, post, parent }, context) => {
if (!context.user) throw new Error('Only logged in user can create new comment');
const newComment = { author: context.user._id, content };
if (post) newComment.post = fromGlobalId(post).id;
if (parent) newComment.parent = fromGlobalId(parent).id;
return Comment.create(newComment);
},
Would be so glad if somebody will provide better experience with this.

Resources