nexus-shield
    TypeScript icon, indicating that this package has built-in type declarations

    2.1.0 • Public • Published

    nexus-shield

    Github Actions codecov npm version

    Help Wanted ⚠️

    If you are a Typescript expert, I could use a hand on a lingering typing issue when a shield parameter is added to an objectType. Please see the related issue for details. Thanks!

    Overview

    Nexus Shield is a nexus plugin that helps you create an authorization layer for your application. It is a replacement for the provided authorization plugin. It is heavily inspired by Graphql Shield and reuses most of it's familiar ruling system. It takes full advantage of the type safety provided by nexus.

    Install

    npm install --save nexus-shield
    
    OR
    
    yarn add nexus-shield

    Usage

    Nexus configuration

    The plugin first needs to be installed in nexus. This will add the new shield parameter. The plugin will work without any provided configuration, but it is recommended to provide one that is relevant to your application. The available parameters are:

    • defaultError: The error that is thrown if the access is denied. See the errors section.
    • defaultRule: Rule that is used if none is specified for a field.
    • hashFunction: Function used to hash the input to provide caching keys.

    For example, using an Apollo server:

    import { nexusShield, allow } from 'nexus-shield';
    import { ForbiddenError } from 'apollo-server';
    
    const schema = makeSchema({
      // ... Rest of the configuration
      plugins: [
        nexusShield({
          defaultError: new ForbiddenError('Not allowed'),
          defaultRule: allow,
        }),
      ],
    });

    Styles

    Two interfaces styles are provided for convenience: Graphql-Shield and Nexus.

    Graphql-Shield

    rule()((root, args, ctx) => {
      return !!ctx.user;
    });

    Nexus

    ruleType({
      resolve: (root, args, ctx) => {
        return !!ctx.user;
      },
    });

    Error

    • A rule needs to return a boolean, a Promise<boolean> or throw an Error.
    • Contrary to Graphql-shield, this plugin will NOT catch the errors you throw and will just pass them down to the next plugins and eventually to the server
    • If false is returned, the configured defaultError will be thrown by the plugin.
    import { AuthenticationError } from 'apollo-server';
    
    const isAuthenticated = ruleType({
      resolve: (root, args, ctx) => {
        const allowed = !!ctx.user;
        if (!allowed) throw new AuthenticationError('Bearer token required');
        return allowed;
      },
    });

    Operators

    Rules can be combined in a very flexible manner. The plugin provides the following operators:

    • and: Returns true if all rules return true
    • or: Returns true if one rule returns true
    • not: Inverts the result of a rule
    • chain: Same as and, but rules are executed in order
    • race: Same as or, but rules are executed in order
    • deny: Returns false
    • allow: Returns true

    Simple example:

    import { chain, not, ruleType } from 'nexus-shield';
    
    const hasScope = (scope: string) => {
      return ruleType({
        resolve: (root, args, ctx) => {
          return ctx.user.permissions.includes(scope);
        },
      });
    };
    
    const backlist = ruleType({
      resolve: (root, args, ctx) => {
        return ctx.user.token === 'some-token';
      },
    });
    
    const viewerIsAuthorized = chain(
      isAuthenticated,
      not(backlist),
      hasScope('products:read')
    );

    Shield Parameter

    To use a rule, it must be assigned to the shield parameter of a field:

    export const Product = objectType({
      name: 'Product',
      definition(t) {
        t.id('id');
        t.string('prop', {
          shield: ruleType({
            resolve: (root, args, ctx) => {
              return !!ctx.user;
            },
          }),
        });
      },
    });

    Type safety

    This plugin will try its best to provide typing to the rules.

    • It is preferable to define rules directly in the definition to have access to the full typing of root and args.
    • The ctx is always typed if it was properly configured in nexus makeSchema.
    • If creating generic or partial rules, use the appropriate helpers (see below).
    export type Context = {
      user?: { id: string };
    };
    
    export const Product = objectType({
      name: 'Product',
      definition(t) {
        t.id('id');
        t.string('ownerId');
        t.string('prop', {
          args: {
            filter: stringArg({ nullable: false }),
          },
          shield: ruleType({
            resolve: (root, args, ctx) => {
              // root => { id: string }, args => { filter: string }, ctx => Context
              return true;
            },
          }),
        });
      },
    });

    Generic rules

    • Generic rules are rules that do not depend on the type of the root or args.
    • The wrapper generic is provided for this purpose. It will wrap your rule in a generic function.
    const isAuthenticated = generic(
      ruleType({
        resolve: (root, args, ctx) => {
          // Only ctx is typed
          return !!ctx.user;
        },
      })
    );
    
    // Usage
    t.string('prop', {
      shield: isAuthenticated(),
    });

    Partial rules

    • Partial rules are rules that depend only on the type of the root.
    • The wrapper partial is provided for this purpose. It will wrap your rule in a generic function.
    const viewerIsOwner = partial(
      ruleType({
        type: 'Product' // It is also possible to use the generic parameter of `partial`
        resolve: (root, args, ctx) => {
          // Both root and ctx are typed
          return root.ownerId === ctx.user.id;
        },
      })
    );
    
    // Usage
    t.string('prop', {
      shield: viewerIsOwner(),
    });

    Combining rules

    If you mix and match generic rules with partial rules, you will need to specify the type in the parent helper.

    const viewerIsAuthorized = partial<'Product'>(
      chain(isAuthenticated(), viewerIsOwner())
    );

    However, if you specify it directly in the shield field, there is not need for an helper thus no need for a parameter.

    t.string('prop', {
      shield: chain(isAuthenticated(), viewerIsOwner()),
    });

    Caching

    • The result of a rule can be cached to maximize performances. This is important when using generic or partial rules that require access to external data.
    • The caching is always scoped to the request

    The plugin offers 3 levels of caching:

    • NO_CACHE: No caching is done (default)
    • CONTEXTUAL: Use when the rule only depends on the ctx
    • STRICT: Use when the rule depends on the root or args

    Usage:

    rule({ cache: ShieldCache.STRICT })((root, args, ctx) => {
      return true;
    });
    
    ruleType({
      cache: ShieldCache.STRICT,
      resolve: (root, args, ctx) => {
        return !!ctx.user;
      },
    });

    Known issues / limitations

    Install

    npm i nexus-shield

    DownloadsWeekly Downloads

    964

    Version

    2.1.0

    License

    MIT

    Unpacked Size

    55.5 kB

    Total Files

    54

    Last publish

    Collaborators

    • sytten