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!
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 its familiar ruling system. It takes full advantage of the type safety provided by nexus.
npm install --save nexus-shield
OR
yarn add nexus-shield
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,
}),
],
});
- When using subscriptions with a server that is not integrated directly into your "main" GraphQL server, you must make sure that you pass in a valid context.
- This context should contain all the information needed to evaluate the rules. Ideally, it is the same as the context for your "main" server otherwise the typing won't reflect the data available to the rules.
For example, using GraphQL-WS:
useServer(
{
schema,
context: (ctx, msg, args) => {
// That will return the same context that was passed when the
// server received the subscription request
return ctx;
},
},
wsServer
);
Two interface styles are provided for convenience: Graphql-Shield
and Nexus
.
rule()((root, args, ctx) => {
return !!ctx.user;
});
ruleType({
resolve: (root, args, ctx) => {
return !!ctx.user;
},
});
- A rule needs to return a
boolean
, aPromise<boolean>
or throw anError
. - 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 configureddefaultError
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;
},
});
Rules can be combined in a very flexible manner. The plugin provides the following operators:
and
: Returnstrue
if all rules returntrue
or
: Returnstrue
if one rule returnstrue
not
: Inverts the result of a rulechain
: Same asand
, but rules are executed in orderrace
: Same asor
, but rules are executed in orderdeny
: Returnsfalse
allow
: Returnstrue
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')
);
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;
},
}),
});
},
});
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 ofroot
andargs
. - The
ctx
is always typed if it was properly configured in nexusmakeSchema
. - 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 are rules that do not depend on the type of the
root
orargs
. - 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 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(),
});
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 no need for a helper thus no need for a parameter.
t.string('prop', {
shield: chain(isAuthenticated(), viewerIsOwner()),
});
- The result of a rule can be cached to maximize performance. 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 thectx
STRICT
: Use when the rule depends on theroot
orargs
Usage:
rule({ cache: ShieldCache.STRICT })((root, args, ctx) => {
return true;
});
ruleType({
cache: ShieldCache.STRICT,
resolve: (root, args, ctx) => {
return !!ctx.user;
},
});
-
Currently, the typing of the
shield
parameter onobjectType
doesn't work. Tracked by issue: #50 -
It is not possible to pass directly an
objectType
to the parametertype
of aruleType
. Tracked by issue: graphql-nexus/nexus#451 -
The helpers are necessary to provide strong typing and avoid the propagation of
any
. See this StackOverflow issue for more on the subject.