ACL Middleware/plugin for GraphQL Factory
A graphql-factory plugin that provides granular control over graphql
operations as well as a graphql api for the acl library itself.
import * as graphql from 'graphql'
import GraphQLFactory from 'graphql-factory'
import ACLPlugin from 'graphql-factory-acl'
import ACL from 'acl'
import jwt from 'jsonwebtoken'
// import a graphql-factory definition that has been
// tagged with _factoryACL
import definition from './definition'
const userId = 'admin@localhost'
const options = { secret: 'abc123' }
const acl = new ACL(new ACL.memoryBackend())
const plugin = new ACLPlugin(acl, options)
const factory = GraphQLFactory(graphql)
const lib = factory.make(definition, {
plugin: [ plugin ]
})
const token = jwt.sign({ userId }, options.secret)
lib.Foo(`
query BarQuery {
readBar {
baz
}
}
`, { jwt: token })
.then(result => {
...
})Tagging
Permissions are applied to an operation by adding them via the
allow mutation (or directly with an acl instance) and tagging
the query/mutation/subscription field with a _factoryACL tag who's
value is the required permission for that operation.
{
schemas: {
Foo: {
query: {
fields: {
Bar: {
type: ['Baz'],
resolve () {...},
_factoryACL: 'read'
}
}
}
}
}
}Resource Path
Resource paths are how this plugin uniquely identifies a schema,
operation type, field, argument, and/or selection. Resource paths can
also be used to specify inherited permissions by ending the path with
an * which indicates that the permission be applied to that path and
everything below it.
For example
// gives the admin role access to all operations in the ACL schema
acl.allow('admin', 'ACL.*', '*')
// allow users role to read all properties of the hasRole query
acl.allow('users', 'ACL.query.hasRole.*', 'read')
// allow the dev role access to read and write only the
// baz field of the mutation Bar in the schema Foo
acl.allow('dev', [
'Foo.mutation.Bar.args.baz',
'Foo.mutation.Bar.selection.baz'
], '*')Explicit deny paths aka !paths can also be allowed in order to
explicitly deny access to a resource path
For example the following permissions would allow the helpdesk role
complete access to the User schema but error on any selection of
the password field. However setting the password is still possible
since !User.mutation.createUser.args.password has not been allowed
acl.allow('helpdesk', [
'User.*',
'!User.mutation.createUser.selection.password',
'!User.query.readUser.selection.password',
'!User.subscription.subscribeUser.selection.password'
], '*')Additionally the resource path * gives access to everything that is
tagged with _factoryACL
Permissions
In general when securing a graphql api using the * permission is
sufficient since the resource path itself contains information on
what type of operation (mutation/query/subscription) is taking place.
Named permissions can however be used to allow partial access to a
schema.
For example if you wanted to allow users full access to the Foo
schemas mutations except for the ability to delete things you can
tag all mutations that delete with a delete permission and the rest
with a write permission and use the following allow
acl.allow('users', 'Foo.mutations.*', 'write')When the permission for delete is evaulated it will fail for the users
role as expected. Of course an alternative is also to add an explicit deny
with !Foo.mutations.deleteFoo.*
JSON Web Tokens (jwt) are used for authorization. When setting up the plugin
a secret option can be supplied and will be used during verification of
any jwt provided in the rootValue.jwt property of the graphql request.
Additionally a systemApiKey option can be set and provided in rootValue.apikey
in order to give unrestricted access. This can be useful for operations like
subscriptions that run for extended periods of time and could potentially encounter
and expired jwt. Or event for server side scheduled tasks that require calls to the
graphql api.
JSON web tokens are used for authorization because they can be rotated and
carry information in their payload. jwt should be generated by an
authentication service that issues jwt signed with the same secret
configured on the acl plugin.
payload
The payload MUST be an object with a userId field containing the requesting
users id. If you wish to use a field other than userId (because your
jwt service puts it in a different path) you can set the userIdField
in the plugin options with a lodash compatible path string.
rotating secrets
The plugin verifies every jwt on each request and looks up the current
secret during that process. This is why the secret value is intentionally
stored in the plugin options object. This way you can keep a reference
to the options object in your main project and simply update the secret
field.
Additionally, when setting up a new ACL database or during development
you may wish to bypass ACL authentication. To bypass all ACL checks
simply do not set a secret in the options. This can be useful when
setting up a new admin user with access to the ACL api to set up
future permissions. Additionally a helper method createAdmin has been
provided to add complete access to the ACL schema.
Creates a new acl plugin
Parameters
acl- An acl instance initialized with a backendoptions- [
schemaName=ACL] {string} - Customizable graphql schema name - [
secret] {string} - Secret to use when signing jsonwebtokens. If omitted no acl rules will be evaluated (should only be omitted during dev or initial setup) - [
systemUserId] {string} - Optional userId that will not be checked against acl rules and will have unrestricted access to all graphql schemas. - [
systemApiKey] {string} - Optional system apikey for unrestricted access. must be passed in therootValue.apikeyproperty.
- [
The plugin provides a complete graphql api for the acl library and
adds additional shortcut/helper methods for managing permissions on
graphql resources. The original acl documentation
is a good place to start for understanding the acl API.
Please refer to the ACL Schema Definition
for included query/mutations. Additional operations added are
- queries
listUsers- provides a list of users
- mutations
allowUserId- same asallowexcept allows a specificuserIdremoveAllowUserId- same asremoveAllowexcept it removes permissions for a specificuserIdallowGraphQL- constructs agraphqlresource path and allows access to itremoveAllowGraphQL- constructs agraphqlresource path and removes access to it