- Install with
npm i --save graphql-field-policy
oryarn add graphql-field-policy
- Import alongside your GraphQL schema:
import { fieldPolicy } from 'graphql-field-policy'
// or
const { fieldPolicy } = require('graphql-field-policy');
The tests, located in the test/
directory, are written as a readable example of implementation.
Previous, public attempts to introduce field-level authorization into GraphQL JS schemas involve one of two approaches:
- GraphQL frameworks or service providers like GraphCool or
- Authorization logic within the resolver
Like me, you may want to steer clear of GraphQL providers, and including authorization logic within the resolver can become unwieldy and easily lend itself to oversight. Consider the following query:
query GetCustomers {
customers {
banks {
accounts {
balance
}
}
}
}
Authorization here has to happen at 6 different levels:
- Should the user be able to query
customers
at all? - Should the user be able to view each customer record returned?
- For each customer record returned, should the user be able to query
banks
? - Should the user be able to view each bank?
- For each bank, should the user be able to view accounts?
- For each account, should the user be able to view the balance?
If you leave your authorization logic to the resolver, it may be difficult to grok your overall approach to user authorization, once your schema becomes more complex.
Using graphql-field-policy
, you'll define policies for Bank
, Account
, and Customer
, keeping authorization logic neatly separated, and then wrap the resolvers at steps 1, 3, and 5 with a simple fieldPolicy()
, keeping your schema and resolvers cleaner and free to focus on business logic.
Those coming from the worlds of Ruby and Rails will see similarities with the excellent pundit
gem, which was the inspiration for this policy model.
JS lacks some of the magic that Pundit relied upon, and so policy implementation here is both simpler and more limited.
Your policies will inherit from this library's Policy
class. If you're using TypeScript, note the two type parameters, ContextType
and RecordType
.
A policy's context defines the environment it operates within, and generally carries information about the authenticated user, and possibly the request itself. That's up to you.
A policy's record type is simply the type of record it will be authorizing against. If you model using class Book
, for example, that's the RecordType
.
A policy has a set of standard methods which you can override:
show
index
create
update
destroy
manage
: access to all methods and fields, generally reserved for admin usersfield(fieldName)
authorize(operation)
: rather than returningfalse
on an unauthorized operation, will throw anUnknownOperationError | NotAuthorizedError
authorizeField(fieldName)
: similar toauthorize
but for individual fields
Only field
, manage
, show
, and authorize*
are currently integrated with GraphQL here, but you can call them directly within your models or resolvers in the meantime.
Policy#field
takes a field name as its only parameter, and returns true
if the field of the policy's guarded record may be read by its user within the given context, and false
otherwise. For example, if a policy
guards a Book
, and that book may have a published
attribute, then you may authorize access to the price
field based on whether or not the user works for the publisher:
field(name: string) {
switch(name) {
case 'price':
return this.record.published || this.context.user.worksForPublisherId = this.record.publisherId
default:
return false;
}
}
show() {
return true
}
In this case, the book itself has been authorized to query fields from because show
returns true
. field
returns an implicit deny with default: return false
, and so all other fields will deny access to all users.
Policy#show
, as shown above, guards read access to the resolved entity as a whole, which maps to a DocumentNode
in your GraphQL Schema.
Policy#manage
authorizes all access to the object and all fields, to prevent repetitive checks like return user.isAdmin
on individual operations and fields. If #manage
returns true
, the user is authorized and no other methods are called. Use with caution.
Policy#authorize
and Policy#authorizeField
are similar, except that they take an operation name (like show
) or a field name (like price
), query it, and throw an error if the return value is false. Within GraphQL, the error will travel back up the tree and be returned to the user.
The root Policy
class has many other methods, such as new
, edit
, and create
, that you can easily use and override for use within your own models - but those operations are not yet used by the Field Policy GraphQL implementation. Before those are implemented, you can use them within your resolvers like so:
const myResolver = (object, args, context, info) => {
new MyPolicy(context, object).authorize('update'); // Will throw an error if unauthorized
return object.doMutatingThing(args);
}
On their own, policies are a good and intuitive way to collect the authorization logic from across your application. Using the fieldPolicy
resolver wrapper, we can adapt those policies to a GraphQL schema.
Two parts are necessary:
- A
policyMap
of GraphQLTypes to Policy classes - A
fieldPolicy
wrapper on each resolver you want to authorize
See the test schema for a working example of fieldPolicy
implementation.
The policyMap
may also have a __default
key which provides a fallback in case no other policies are available. In most cases, this is discouraged because of the small number of policies actually required to maintain the mapping, and because a default policy may cause unintended and overlooked false positives or negatives in authorization.
There are two planned features in development:
- Authorization for mutations and subscriptions
- A single authorization wrapper around the entire GraphQL schema, to prevent having to wrap each individual resolver in a
fieldPolicy
Contributions are welcome! The repo has resources to help you get started:
-
The
.vscode/
directory contains the settings to hook the debugger into Jest test runs -
Jest tests can be run with
yarn test
-
If you want to develop/test this module alongside another package, you'll have to take a couple steps in order to avoid an error that looks like this:
Cannot use GraphQLList "[Request]" from another module or realm. Ensure that there is only one instance of "graphql" in the node_modules directory. If different versions of "graphql" are the dependencies of other relied on modules, use "resolutions" to ensure only one version is installed. https://yarnpkg.com/en/docs/selective-version-resolutions Duplicate "graphql" modules cannot be used at the same time since different versions may have different capabilities and behavior. The data from one version used in the function from another could produce confusing and spurious results.
rm -rf node_modules
within this package to clear those outyarn install --production
to install without thedevDependencies
- Within your other project, where
graphql
is installed:
cd node_modules/graphql && yarn link
- If using typescript, then
cd ../@types/graphql && yarn link
- Return to this package and run
yarn link "graphql" && yarn link "@types/graphql"
. Now this package will build using the graphql version from your other project.