graphql-shield
GraphQL Server permissions as another layer of abstraction!
Overview
GraphQL Shield helps you create a permission layer for your application. Using an intuitive rule-API, you'll gain the power of the shield engine on every request and reduce the load time of every request with smart caching. This way you can make sure your application will remain quick, and no internal data will be exposed.
Try building a groceries shop to better understand the benefits of GraphQL Shield! Banana &Co. πππ.
Explore common receipts and learn about advanced GraphQL! GraphQL Shield 3.0 βοΈπ‘π΄.
Features
- βοΈ Flexible: Based on GraphQL Middleware.
- π Easy to use: Just add permissions to your Yoga
middlewares
set, and you are ready to go! - π€ Compatible: Works with all GraphQL Servers.
- π Smart: Intelligent V8 Shield engine caches all your request to prevent any unnecessary load.
- π― Per-Type: Write permissions for your schema, types or specific fields (check the example below).
Install
yarn add graphql-shield
Example
GraphQL Yoga
import { GraphQLServer } from 'graphql-yoga'
import { rule, shield, and, or, not } from 'graphql-shield'
const typeDefs = `
type Query {
frontPage: [Fruit!]!
fruits: [Fruit!]!
customers: [Customer!]!
}
type Mutation {
addFruitToBasket: Boolean!
}
type Fruit {
name: String!
count: Int!
}
type Customer {
id: ID!
basket: [Fruit!]!
}
`
const resolvers = {
Query: {
frontPage: () => [
{ name: 'orange', count: 10 },
{ name: 'apple', count: 1 },
],
},
}
// Auth
const users = {
mathew: {
id: 1,
name: 'Mathew',
role: 'admin',
},
george: {
id: 2,
name: 'George',
role: 'editor',
},
johnny: {
id: 3,
name: 'Johnny',
role: 'customer',
},
}
function getUser(req) {
const auth = req.get('Authorization')
if (users[auth]) {
return users[auth]
} else {
return null
}
}
// Rules
const isAuthenticated = rule()(async (parent, args, ctx, info) => {
return ctx.user !== null
})
const isAdmin = rule()(async (parent, args, ctx, info) => {
return ctx.user.role === 'admin'
})
const isEditor = rule()(async (parent, args, ctx, info) => {
return ctx.user.role === 'editor'
})
// Permissions
const permissions = shield({
Query: {
frontPage: not(isAuthenticated),
fruits: and(isAuthenticated, or(isAdmin, isEditor)),
customers: and(isAuthenticated, isAdmin),
},
Mutation: {
addFruitToBasket: isAuthenticated,
},
Fruit: isAuthenticated,
Customer: isAdmin,
})
const server = GraphQLServer({
typeDefs,
resolvers,
middlewares: [permissions],
context: req => ({
...req,
user: getUser(req),
}),
})
server.start(() => console.log('Server is running on http://localhost:4000'))
graphql-middleware
Others, using // Permissions...
// Apply permissions middleware with applyMiddleware
// Giving any schema (instance of GraphQLSchema)
import { applyMiddleware } from 'graphql-middleware'
// schema definition...
schema = applyMiddleware(schema, permissions)
API
Types
// Rule
function rule(name?: string, options?: IRuleOptions)(func: IRuleFunction): Rule
export type IFragment = string
export type ICacheOptions = 'strict' | 'contextual' | 'no_cache' | boolean
export type IRuleResult = boolean | Error
export type IRuleFunction = (
parent?: any,
args?: any,
context?: any,
info?: GraphQLResolveInfo,
) => IRuleResult | Promise<IRuleResult>
interface IRuleOptions {
cache?: ICacheOptions
fragment?: IFragment
}
// Logic
function and(...rules: IRule[]): LogicRule
function or(...rules: IRule[]): LogicRule
function not(rule: IRule): LogicRule
const allow: LogicRule
const deny: LogicRule
export type ShieldRule = IRule | ILogicRule
interface IRuleFieldMap {
[key: string]: IRule
}
interface IRuleTypeMap {
[key: string]: IRule | IRuleFieldMap
}
export type IRules = ShieldRule | IRuleTypeMap
function shield(rules?: IRules, options?: IOptions): IMiddleware
export interface IOptions {
debug?: boolean
allowExternalErrors?: boolean
whitelist?: boolean
fallback?: string | Error
}
shield(rules?, options?)
Generates GraphQL Middleware layer from your rules.
rules
A rule map must match your schema definition. All rules must be created using the rule
function to ensure caches are made correctly. You can apply your rule
accross entire schema, Type scoped, or field specific.
Limitations
- All rules must have a distinct name. Usually, you won't have to care about this as all names are by default automatically generated to prevent such problems. In case your function needs additional variables from other parts of the code and is defined as a function, you'll set a specific name to your rule to avoid name generation.
// Normal
const admin = rule({ cache: 'contextual' })(
async (parent, args, ctx, info) => true,
)
// With external data
const admin = bool =>
rule(`name-${bool}`, { cache: 'contextual' })(
async (parent, args, ctx, info) => bool,
)
- Cache is enabled by default accross all rules. To prevent
cache
generation, set{ cache: 'no_cache' }
or{ cache: false }
when generating a rule. - By default, no rule is executed more than once in complete query execution. This accounts for significantly better load times and quick responses.
Cache
You can choose from three different cache options.
no_cache
- prevents rules from being cached.contextual
- use when rule only relies onctx
parameter.strict
- use when rule relies onparent
orargs
parameter as well.
// Contextual
const admin = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
return ctx.user.isAdmin
})
// Strict
const admin = rule({ cache: 'strict' })(async (parent, args, ctx, info) => {
return ctx.user.isAdmin || args.code === 'secret' || parent.id === 'theone'
})
Backward compatiblity:
{ cache: false }
converts tono_cache
, and{ cache: true }
converts tostrict
.
options
Property | Required | Default | Description |
---|---|---|---|
allowExternalErrors | false | false | Toggle catching internal errors. |
debug | false | false | Toggle debug mode. |
whitelist | false | false | Whitelist rules instead of blacklisting them. |
fallback | false | Error('Not Authorised') | Error Permission system fallbacks to. |
By default shield
ensures no internal data is exposed to client if it was not meant to be. Therefore, all thrown errors during execution resolve in Not Authenticated!
error message if not otherwise specified using error
wrapper. This can be turned off by setting allowExternalErrors
option to true.
allow
, deny
GraphQL Shield predefined rules.
allow
and deny
rules do exactly what their names describe.
and
, or
, not
and
,or
andnot
allow you to nest rules in logic operations.
And Rule
And
rule allows access only if all sub rules used return true
.
Or Rule
Or
rule allows access if at least one sub rule returns true
and no rule throws an error.
Not
Not
works as usual not in code works.
import { shield, rule, and, or } from 'graphql-shield'
const isAdmin = rule()(async (parent, args, ctx, info) => {
return ctx.user.role === 'admin'
})
const isEditor = rule()(async (parent, args, ctx, info) => {
return ctx.user.role === 'editor'
})
const isOwner = rule()(async (parent, args, ctx, info) => {
return ctx.user.items.some(id => id === parent.id)
})
const permissions = shield({
Query: {
users: or(isAdmin, isEditor),
},
Mutation: {
createBlogPost: or(isAdmin, and(isOwner, isEditor)),
},
User: {
secret: isOwner,
},
})
Custom Errors
Shield, by default, catches all errors thrown during resolver execution. This way we can be 100% sure none of your internal logic will be exposed to the client if it was not meant to be.
To return custom error messages to your client, you can return error instead of throwing it. This way, Shield knows it's not a bug but rather a design decision.
const typeDefs = `
type Query {
customError: String!
}
`
const resolvers = {
Query: {
customError: () => {
return new Error('customErrorResolver')
},
},
}
const permissions = shield()
const server = GraphQLServer({
typeDefs,
resolvers,
middlewares: [permissions],
})
Global Fallback
GraphQL Shield allows you to set a globally defined fallback that is used instead of Not Authorised!
default response. This might be particularly useful for localisation. You can use string
or even custom Error
to define it.
const permissions = shield({
Query: {
items: allow
},
}, {
fallback: "To je napaka!" // meaning "This is a mistake" in Slovene.
})
const permissions = shield({
Query: {
items: allow
},
}, {
fallback: new CustomError("You are something special!")
})
Fragments
Fragments allow you to define which fields your rule requires to work correctly. This comes in extremely handy when your rules rely on data from database. You can use fragments to define which data your rule relies on.
const isItemOwner = rule({
cache: "strict",
fragment: "fragment ItemID on Item { id }"
})(async ({ id }, args, ctx, info) => {
return ctx.db.exists.Item({
id,
owner: { id: ctx.user.id }
})
})
const permissions = shield({
Query: {
items: allow
},
Item: {
id: allow,
name: allow,
secret: isItemOwner
}
}, {
whitelist: true
})
Whitelisting vs Blacklisting
Shield allows you to lock-in your schema. This way, you can seamleslly develop and publish your work without worrying about exposing your data. To lock in your service simply set whitelist
to true
like this;
const typeDefs = `
type Query {
users: [User!]!
newFeatures: FeaturesConnection!
}
type User {
id: ID!
name: String!
author: Author!
}
type Author {
id: ID!
name: String!
secret: String
}
`
const permissions = shield({
Query: {
users: allow,
},
User: allow,
Author: {
id: allow,
name: allow,
}
})
You can achieve same functionality by setting every "rule-undefined" field to deny
the request.
Contributors
This project exists thanks to all the people who contribute. [Contribute].
Backers
Thank you to all our backers! π [Become a backer]
Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]
Contributing
We are always looking for people to help us grow graphql-shield
! If you have an issue, feature request, or pull request, let us know!
License
MIT @ Matic Zavadlal