Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom errors for rules and global fallback #60

Closed
m-basov opened this issue Jun 17, 2018 · 23 comments
Closed

Custom errors for rules and global fallback #60

m-basov opened this issue Jun 17, 2018 · 23 comments
Assignees
Labels
kind/feature A request for a new feature.

Comments

@m-basov
Copy link
Contributor

m-basov commented Jun 17, 2018

Hi @maticzav! First of all thanks for the awesome library!

I have few questions/suggestions about CustomErrors:

  1. Is there a way to specify CustomError that will be returned from shield in case if permissions checks are failed?

Example:
I have isAuthenticated rule

export default rule()((parent, args, { user }) => Boolean(user));

I want to return my custom 401 error like this

import { SevenBoom } from 'graphql-apollo-errors';

rule(null, { 
  customError: SevenBoom.unauthorized('You are not authenticated')
})((parent, args, { user }) => Boolean(user))
  1. Is there a way to specify global CustomError instead of https://github.com/maticzav/graphql-shield/blob/master/src/index.ts#L234 which will be used in case if there is no rule specific error?

Example:

import { SevenBoom } from 'graphql-apollo-errors';

shield({ /* Rules… */ }, {
  customError: SevenBoom.forbidden('You are not allowed to do this action.')
})

I can implement this and looks like it is not a breaking change.

Just want to clarify if there is no way to do this in current version of the library?

Let me know if you have any questions. Thanks!

@maticzav
Copy link
Owner

@kolybasov this is great. I have nothing to add to second question, let’s make a PR! 🎉

Right now, there are two ways to solving the first question. You could use allowExternalErrors which will allow all errors that happen during execution to pass through. The second one is by throwing CustomError in rule evaluation.

Hope this answers your question 🙂

@maticzav maticzav added the kind/feature A request for a new feature. label Jun 19, 2018
@m-basov
Copy link
Contributor Author

m-basov commented Jun 19, 2018

@maticzav thanks for the fast response!

  1. Not sure if we understand each other correctly on this one. I will try to explain more clear.

Let's imagine I have 2 rules: isAuthenticated and isAdmin.

// isAuthenticated.js
import { shield, rule, and } from 'graphql-shield';

// User should be present in context
const isAuthenticated = rule()((parent, args, ctx) => Boolean(ctx.user));
// User role should be equal to 'admin'
const isAdmin = rule()((parent, args, ctx) => ctx.user.role === 'admin');

// I am combining this 2 rules together
const permissions = shield({
  Query: {
    getAllUsers: and(isAuthenticated, isAdmin)
  }
});

My goal to have this:

  1. If there is no user in the context(isAuthenticated is false) then throw 401 error.
  2. If the user is present but role is not an admin(isAdmin rule is false) then throw 403 error.

If I understood correctly now we always will have CustomError('Not Authorised!') if rules check was failed, right?

  1. Adding global customizable default error is fine, right?

Thanks for helping with this!

@maticzav
Copy link
Owner

Hey 👋

Almost correct; logical operators, though, don't act like new rules (they should act like logical operators!). So, you can do something like this:

// isAuthenticated.js
import { shield, rule, and, CustomError } from "graphql-shield";

// User should be present in context
const isAuthenticated = rule()((parent, args, ctx) => {
  if (ctx.user) {
    return true;
  } else {
    throw new CustomError(401);
  }
});
// User role should be equal to 'admin'
const isAdmin = rule()((parent, args, ctx) => {
  if (ctx.user.role === 'admin') {
    return true;
  } else {
    throw new CustomError(403);
  }
});

// I am combining this 2 rules together
const permissions = shield({
  Query: {
    getAllUsers: and(isAuthenticated, isAdmin)
  }
});

As you can see, if one of the rules fails it will return desired error and vice versa.

And yes, big time! I would love to see a PR for customizable default error. I hope this helps you out. 🙂

@m-basov
Copy link
Contributor Author

m-basov commented Jun 19, 2018

And in case of or operator it will still throw an error, right?

import { shield, rule, or, CustomError } from "graphql-shield";

// User should be present in context
const isAuthenticated = rule()((parent, args, ctx) => {
  if (ctx.user) {
    return true;
  } else {
    throw new CustomError(401);
  }
});
// User role should be equal to 'admin'
const isAdmin = rule()((parent, args, ctx) => {
  if (ctx.user.role === 'admin') {
    return true;
  } else {
    throw new CustomError(403);
  }
});

// I am combining this 2 rules together
const permissions = shield({
  Query: {
    getAllUsers: or(isAuthenticated, isAdmin)
  }
});

So if I need only some of this rules then throwing custom error is not the case. Correct?

@maticzav
Copy link
Owner

The first question, indeed, it will work with all logical operators includingor. I am not sure I understand the second one, though. Both of these rules work with or without being combined with or. Therefore, they will act the same way - considering errors - once they are combined or used alone.

Is this what you had in mind?

@m-basov
Copy link
Contributor Author

m-basov commented Jun 19, 2018

Sorry for the confusion.

For me it looks like when I need to check against few rules combined with or and first rule will be false but second rule will result to true shield will throw an error.

My example don't plays really nice together(treat it as pseudocode) but if we will take both rules isAuthenticated and isAdmin and combine them with or(isAuthenticated, isAdmin) then rule never pass if isAuthenticated will throw an error. But it should pass if we will use booleans, right?

@maticzav
Copy link
Owner

OK, I can see it now. I think we should find a way to combine the CustomError somehow to make it work with or. I think this is more of a philosophical question in a way. Therefore, I would love to have a short discussion here before making changes.

My thinking process looks like this;

  • We want to reduce the number of unexpected errors as much as possible.
  • We also want to put privacy/security in front of functionality because we can fix functionality but can't fix security with no consequences.
  • I am quite certain that errors are not a good option in any case.
  • and is pretty clear since everything should work for it to pass.
  • or and not are a bit different in a way.

Maybe the best option would be to return an error instead of throwing it since this also better aligns with code writing philosophy in my opinion.

To conclude, I propose to change the functionality by adding customError or error function which is returned from the rule in case of "failure".

Nevertheless, if anything throws an error, the rule should fail. I believe this is crucial for reducing unexpected errors.

What do you think?

@maticzav maticzav self-assigned this Jun 28, 2018
@maticzav maticzav added this to To do in GraphQL Shield 3.0 Jul 6, 2018
@maticzav maticzav moved this from To do to In progress in GraphQL Shield 3.0 Jul 9, 2018
@maticzav maticzav moved this from In progress to Done in GraphQL Shield 3.0 Jul 19, 2018
@m-basov
Copy link
Contributor Author

m-basov commented Aug 7, 2018

Hey @maticzav! I am late a little bit :)

Totally agree with this:

Nevertheless, if anything throws an error, the rule should fail. I believe this is crucial for reducing unexpected errors.

Seems the best option in my opinion!

Maybe the best option would be to return an error instead of throwing it since this also better aligns with code writing philosophy in my opinion.

To conclude, I propose to change the functionality by adding customError or error function which is returned from the rule in case of "failure".

Any help required with this? I see you already started to implement some parts. Thanks!

@maticzav maticzav closed this as completed Aug 9, 2018
@maticzav maticzav reopened this Aug 9, 2018
GraphQL Shield 3.0 automation moved this from Done to In progress Aug 9, 2018
@maticzav
Copy link
Owner

Hey @kolybasov, I need some help. Could you give me your opinion on this; In a hypothetical situation, a user has defined a rule with a custom error. Unfortunately, they weren't precise and made a mistake in their resolver (not rule). Should the rule return customMessage or fallback by default?

@m-basov
Copy link
Contributor Author

m-basov commented Aug 14, 2018

@maticzav I imagine it this way:

import { rule, and } from 'graphql-shield';

const isAuthenticated = rule()((parent, args, { user }) => {
  if (user) return true;
  return new Error('Not Authenticated');
});

const isAdmin = rule()((parent, args, { user }) => {
  if (user.admin) return true;
  return new Error('You have no permission to do this');
});

const canBanUser = and(isAuthenticated, isAdmin);
type Mutation {
  banUser(userId: ID!): Boolean
}
  • If isAuthenticated rule fails we raise Not Authenticated error.
  • If isAdmin rule fails we raise You have no permission to do this error.
  • If there is unhandled exception and allowExternalErrors: true we raise original error.
  • If there is unhandled exception and allowExternalErrors: false we raise fallback or default error.
  • If there is unhandled exception and debug: true original error should always be raised.

Hope it makes sense. Let me know if you have more questions :)

And thank you very much for handling this!

@m-basov
Copy link
Contributor Author

m-basov commented Aug 14, 2018

@maticzav so basically, graphql-shield rule can be in 4 states.

  • true – all is fine
  • false – rule is failed, use default/fallback error
  • ReturnedError instanceof Error – rule is failed, use returned error
  • Unhandled Exception – rules is failed, use default/fallback error depends on debug and allowExternalErrors options.

Personally, I do not use graphql-shield ability to hide internal errors. I format them with graphql-apollo-errors https://github.com/GiladShoham/graphql-apollo-errors#usage

@maticzav
Copy link
Owner

Ok, so my current implementation works like this; I used your first suggestion over mine because it seemed better in the meantime.

const isAdmin = rule({
  error: "You don't have permission to do this."
})(async (parent, args, ctx, info) => {
  if (ctx.user) {
    return true
  } else {
    return false
  }
})

// no user -> error "You don't have permission to do this."

const isOwner = rule({
  error: new ApolloError('Something')
})(async (parent, args, ctx, info) => {
  if (ctx.user) {
    return true
  } else {
    return false
  }
})

// no user -> ApolloError "Something."

Now that I think about it since having the ability to throw an error from inside the rule gives us the ability to access parent, args, ctx, info etc. This might be useful. Nevertheless, your approach from above seems clearer. At the same time, I believe throwing errors is not the best practice.

What do you think?

@m-basov
Copy link
Contributor Author

m-basov commented Aug 14, 2018

@maticzav sorry for the initial confusion I was not sure about best solution also.
I am suggesting not to throw error but return it. So abstract rules check will looks like this:

const result = await rule.resolve(parent, args, ctx, info);

if (result instanceof Error) {
  throw result;
} else if (result === false) {
  throw new CustomError('Not Authorised!'); // Current default error, also make it replacebels via global config(IOptions interface)
} else {
  // all is ok
  return resolver(parent, args, ctx, info);
}

@maticzav
Copy link
Owner

I see... Should we make both cases possible? Can you imagine a scenario where the first idea would make more sense?

@m-basov
Copy link
Contributor Author

m-basov commented Aug 14, 2018

@maticzav I believe they are just alternatives to each other and it is just matter of preference how you would like to specify custom error.

In my opinion returning an error instead of passing it as param gives more flexibility because then rule may have many custom errors specified in case you need them for some complex one. And it is hard to imagine implemented with passing an error as param.

rule()((parent, args, ctx, info) => {
  if (args.something) {
    return new Error('Something');
  } else if (args.somethingElse) {
    return new Error('Something else');
  } else if (args.evenMore) {
    return new Error('Even more!');
  }	
  return true;
});

@maticzav
Copy link
Owner

I agree, the new code already reflects that. @kolybasov let me know if you have a bit of spare time, I could really use a hand to write docs and examples. You could also give feedback on the changes and introduce the new ones. Tell me if that's an option for you!

@maticzav maticzav moved this from In progress to Done in GraphQL Shield 3.0 Aug 14, 2018
@m-basov
Copy link
Contributor Author

m-basov commented Aug 14, 2018

@maticzav sure! Just let me know what I can do and I will help!

@maticzav
Copy link
Owner

Perfect! Are you already in Prisma Slack? If not, could you join it here and find me at maticzav. I think we can better communicate there.

@m-basov
Copy link
Contributor Author

m-basov commented Aug 14, 2018

@maticzav I will join and ping you tomorrow then!

@m-basov
Copy link
Contributor Author

m-basov commented Aug 15, 2018

@maticzav by some reason I can not receive invite to Prisma Slack. Any chance you are using Prisma Spectrum chat? https://spectrum.chat/prisma

@maticzav
Copy link
Owner

Just released the new version, going to close the issue now.

@reergymerej
Copy link

I’m a few years late, but this seems to be a general problem with functions doing more than one thing. Rules should just test and return a boolean. That’s a separate concern from deciding which error to throw.

@fullStackDataSolutions
Copy link

Is there no way to just map error messages to rules? That seems like the way to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature A request for a new feature.
Projects
No open projects
Development

No branches or pull requests

4 participants