The perfect companion of
graphql-shieldto manage your permission layer following BREAD convention
GraphQL Sword is a middleware for a GraphQL Server which helps you create a permission layer with graphql-shield, but following the BREAD permission structure.
The actual behavior is so inspired by the permissions logic from GCF (aka GraphCool Framework).
yarn add graphql-sword graphql-middlewareimport * as express from 'express'
import * as jwt from 'jsonwebtoken'
import { ApolloServer } from 'apollo-server-express'
import { makeExecutableSchema } from 'graphql-tools'
import { applyMiddleware } from 'graphql-middleware'
import { permissions, IPermission } from 'graphql-sword'
const app: express.Express = express()
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}]
}
}
const schema = makeExecutableSchema({
resolvers,
typeDefs,
})
// Queries
// You can directly check information from the context
const isAdminQuery = async (ctx): boolean => return ctx.user.role === 'admin'
// Or request Prisma to verify a specific rule
const isEditorQuery = async (ctx, node_id): boolean =>
ctx.db.exists.User<boolean>({
id: node_id,
role: 'editor',
})
const isCustomer = async (ctx, node_id): boolean =>
ctx.db.exists.Customer<boolean>({
id: node_id,
})
// Permissions
const permissionsSetup: IPermission[] = [
{
operation: 'Fruit.Browse',
alias: 'frontPage',
},
{
operation: 'Fruit.Browse',
authenticated: true,
query: isEditorQuery,
},
{
operation: 'Customer.Browse',
authenticated: true,
query: isAdminQuery,
},
{
operation: 'Customer.Edit',
authenticated: true,
alias: 'addFruitToBasket',
fields: ['basket'],
query: isCustomer,
},
// Will be available later to add rule on a type directly
// {
// operation: 'Customer.*',
// query: isAdminQuery,
// },
]
// Middleware
const permissionsMiddlewareInternal = permissions(
permissionsSetup,
)
const schemaWithMiddleware = applyMiddleware(
schema,
permissionsMiddlewareInternal,
)
const server = new ApolloServer({
schema: schemaWithMiddleware,
context: ctx => ({
...ctx,
db, // any Prisma instance
user: (<any>ctx.req).user || null,
}),
})
// Add some code here to handle JWT / context.user
// See some example from `prisma`, `graphql-yoga`, `apollo-server`
// or `graphql-authentication` repositories
server.applyMiddleware({ app, path: '/' });
app
.listen(options, () => console.info(
`🚀 Server ready at http://localhost:${options.port}${server.graphqlPath}`,
))// Permission
function permission(
permissionsRules: IPermission[],
_options?: IOptions,
): IMiddlewareGenerator<any, any, any>
interface IPermission {
operation: string
authenticated?: boolean
alias?: string
fields?: string[]
query?: IPermissionQuery
cache?: ICacheOptions
}
type IAction = 'Browse' | 'Read' | 'Edit' | 'Add' | 'Delete' | '*'
type ICache = ICacheOptions // inherited from graphql-shield
type IPermissionQuery = (ctx: any, T: any) => Promise<boolean>
interface IOptions {
debug?: boolean
authenticatedRule?: IPermissionRule
}
interface IPermissionRule {
(
args?: IPermissionArgs,
): Rule
}
interface IPermissionArgs {
query?: IPermissionQuery
fields?: string[]
cache?: ICacheOptions
action?: IAction
}Generates GraphQL Middleware layer from your permissions.
An array of your wanted permissions. A permissions must contain the operation name and at lease one option. Actually, a permission is applied on query (Query or Mutation).
It will be available later to apply it on a type or field.
Permissions are appended to a query directly to enforce the validation before the server make the request.
If you apply the fields options to a permission, it will also monitor it before the request.
Because when a permission is applied to a type (or one of its fields), the verification will be done on the response of the request (so the result of the resolver), because the resolver need to populate the parent parameter from (parent, args, context, info) => { ... }.
More example are availabe here.
| Property | Required | Default | Description |
|---|---|---|---|
| debug | false | false | Enable debug mode for graphql-shield. |
| authenticatedRule | false | defaultAuthenticatedRule | Allow the specify a custom rule for the validtion of the authencated option |
By default, graphql-sword control the authenticated option by checking if user and user.id properties are available inside the context. The user property is actullay set during the initialization of the GraphQL Server.
In the example given above, the user is set from ctx.req which represent the Request object from express handled by apollo-server.
For example, with graphql-yoga, it must be get from ctx.request. But you can also add your own logic to get the user from a JWT token or the server session.
- Improve fields checking for the input args for
AddandEditaction, instead of the output fields
- Manage overrided permissions with
or()rule fromgraph-shield
this problem can happen with this config, because there is 2 permissions which result on the same request name
{ operation: 'User.Read', authenticated: true }, { operation: 'User.Read', query: isOwner, }
- Think about an
denyoption to reflect of thenot()rule fromgraphql-shield - Think about an
groupoption to reflect of theand()rule fromgraphql-shield
- Allow CRUD keywords (
Create,Read,Update,Delete) to generate default resolver name likecreateUserorupdateUser - Allow string array for
aliasoption to apply the same permission on several queries
- Allow to apply a permission on a type directly (and not only to the operation
QueryorMutation) - Will need to use fragments on the resolvers with the release of
graphql-shield v3 - Add the option
exclude: string[]to the model permission to add an expection on some request
- Allow to override the
defaultRuleapplied when the optionqueryis used (surely with an optionrule)
MIT @ Johann Pinson
The icon used in the logo is made from Icon Fonts and is licensed by CC BY 3.0