Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Passing parent, args and context into a custom perm auth scope #103

Closed
mmahalwy opened this issue Jul 19, 2021 · 2 comments
Closed

Passing parent, args and context into a custom perm auth scope #103

mmahalwy opened this issue Jul 19, 2021 · 2 comments

Comments

@mmahalwy
Copy link
Contributor

mmahalwy commented Jul 19, 2021

Couple of things:

  1. When to use customPerm, authScopes function and grantScopes? In the docs:
    customPerm
// scope loader with argument
 customPerm: (perm) => context.permissionService.hasPermission(context.User, perm),

authScopes

authScopes: (article, args, context, info) => {
        if (context.User.id === article.author.id) {
          // If user is author, let them see it
          // returning a boolean lets you set auth without specifying other scopes to check
          return true;
        }

        // If the user is not the author, require the employee scope
        return {
          employee: true,
        };
      },

grantScopes

grantScopes: (parent, args, context, info) => ['readArticle'],

Especially when coupled with CASL, I am a little confused with what's the best method to use when using an abilities library. What are the pros/cons of each and when should each of those methods be used? I want to say grantScopes is super powerful but not sure when to use it 😅

  1. In the authScopes initializer, only context is passed. The only way to get parent, args and context is to define authScopes function on the field level. Is this the preferred method when checking auth based on args? What if using customPerm where a field would pass in a perm, is it possible to have parent, args, context passed into that method?

  2. For return type, is there planned support? I guess best workaround is to throw ForbiddenError in the resolver or authScopes on the returned object?

@hayes
Copy link
Owner

hayes commented Jul 20, 2021

Auth is a complicated topic, lots of opinions, strategies and tradeoffs that can be made.

  1. All three of these are things that can be used and combined in different ways, so there isn't a concrete answer here.
    1. scope loaders (eg customPerm): This pattern is useful for integrating with other auth libraries or permissions systems. It allows you to have a place on fields and types where you can define a parameters that will be passed to another library. This is what I would recommend for something like CASL (I'll add more detail below), but it is very flexable and can be used in a number of different ways.
    2. authScopes as a function on a field: This is useful if the scopes needed to resolve a field are dependent on the parent, or args for that field, but could also be used to add auth checks that fall outside your normal auth scopes. I would expect this mostly only to be used for weird edge cases.
    3. authScopes as a function on a type: Similar to authScopes on fields, this is useful if your checks require access to the parent object. I'll include an example of this in my suggestion for CASL below.
    4. grantScopes: This is useful for contextual auth stuff. It's not the most common thing, but for example, if you have some PII like a phone number that only the owner of that data should be able to read, but need to make it available to a support agent working on a ticket for that user.
      With a query like query { ticket(id: 123) { customer: { phoneNumber } } }, the customer field could grant a scope that is checked by the phoneNumber on the User type, so that in the context of a ticket an agent can access the phoneNumber of a customer (as long as they can load the ticket). This allows the User type not to need to worry about support tickets, or add any ticket related auth checks. This is probably not very common, but can be quite powerful.
  2. authScopes only receives context because it is run at the start of the beginning of the operation, so that scopes are initialized before resolving fields. One of the big design considerations with the auth API is performance, and efficiency. scope checks are designed to be cachable and are shared across fields. This is why scope loaders are NOT passed the parent, args, or info objects, and will likely not change. There is a workaround, which is to pass data through the scope parameter (see CASL example below).
  3. Support for accessing return values is not currently planned, but I am open to suggestions on how to handle this if you have specific use cases in mind.

CASL

Here is an example using a couple of the things described above. This is a rough idea based on a brief look at the CASL docs.

Setup:

  1. Add ability to context
  2. Add casl scope loader to types that accepts action and subject in its type param
  3. Call ability.can with action and subject from scope loader
function createContext() {
  const user = getLoggedInUser();
  const ability = defineAbilitiesFor(user);

  return {
    user,
    ability
  }
}

const builder = new SchemaBuilder<{
  Context: {
    user: User,
    ability: Ability,
  };
  AuthScopes: {
    casl: [action: string, subject: unknown],
  };
}>({
  plugins: [ScopeAuthPlugin],
  authScopes: async (context) => ({
    casl: ([action, subject]) => context.ability.can(action, resource),
  }),
});

Auth for a field:

builder.queryField('example', (t) => t.string({
  authScopes: { casl: ['someAction', 'SomeSubject'] },
  resolve: () => 'hi',
}));

Auth for a type (applies to all fields)

builder.objectType('Thing', {
  authScopes: {
    casl: ['someAction', 'SomeSubject'],
  },
  fields: () => ({}),
});

Auth for field that uses the parent (instance of subject):

builder.objectField('ExampleType', 'exampleField',  (t) => t.string({
  authScopes: (parent) => ({ casl: ['someAction',  parent] }),
  resolve: () => 'hi',
}));

Auth for a type using parent (applies to all fields)

builder.objectType('Thing', {
  authScopes: parent => ({
    casl: ['someAction', parent],
  }),
  fields: () => ({}),
});

I put these descriptions and examples together pretty quickly, so there are probably lots of typos, and few other mistakes, but hopefully the general explanations make sense. For future questions like this, I think github discussions might be a better fit than issues. I'd like to track common questions like this there so they are easier for others to learn from without having a bunch of open issues.

@hayes
Copy link
Owner

hayes commented Jul 20, 2021

To elaborate a bit on the CASL example above, I would probably use something like ['read', 'SomeResource'] on the 'SomeResource' type, and then add additional field level scopes for mutations like ['create', 'SomeResource'].

Repository owner locked and limited conversation to collaborators Jul 20, 2021
@hayes hayes closed this as completed Jul 20, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants