The perfect companion of
graphql-shield
to 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-middleware
import * 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
Add
andEdit
action, 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
deny
option to reflect of thenot()
rule fromgraphql-shield
- Think about an
group
option to reflect of theand()
rule fromgraphql-shield
- Allow CRUD keywords (
Create
,Read
,Update
,Delete
) to generate default resolver name likecreateUser
orupdateUser
- Allow string array for
alias
option to apply the same permission on several queries
- Allow to apply a permission on a type directly (and not only to the operation
Query
orMutation
) - 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
defaultRule
applied when the optionquery
is 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