Whitelist GraphQL introspection types returned by apollo-server
import { middleware as whitelistMiddleware } from './utils/introspecton-whitelist'; | |
import { whitelist as introspectionWhitelist } from './utils/introspecton-whitelist/whitelist'; | |
// Your express app | |
// Whitelist GraphQL introspection responses | |
app.use(whitelistMiddleware(introspectionWhitelist)); | |
// Apollo middleware must be below whitelisting middleware |
import { createHash } from 'crypto'; | |
import { NextFunction, Request, Response } from 'express'; | |
import LRU from 'lru-cache'; | |
/* eslint-disable no-underscore-dangle */ | |
interface IntospectionResponse { | |
data: { | |
__schema: { | |
types: { | |
kind: string; | |
name: string; | |
fields: { | |
name: string; | |
}[]; | |
}[]; | |
}; | |
}; | |
} | |
export type WhitelistEntry = | |
| { | |
kind?: string; | |
name: string; | |
fields?: string[]; | |
} | |
| string; | |
export type Whitelist = WhitelistEntry[]; | |
export function withWhitelist(whitelist: Whitelist, response: unknown): IntospectionResponse { | |
const responseTyped = response as IntospectionResponse; | |
return { | |
...responseTyped, | |
data: { | |
...responseTyped.data, | |
__schema: { | |
...responseTyped.data.__schema, | |
types: responseTyped.data.__schema.types.reduce((prev, type) => { | |
const entry = whitelist.find(_ => { | |
if (typeof _ === 'string') { | |
return _ === type.name; | |
} | |
return _.name === type.name && (_.kind === undefined || _.kind === type.kind); | |
}); | |
if (!entry) { | |
return prev; | |
} | |
const allowedFields = typeof entry === 'string' ? undefined : entry.fields; | |
if (!allowedFields) { | |
return [...prev, type]; | |
} | |
const { fields } = type; | |
return [ | |
...prev, | |
{ | |
...type, | |
fields: fields.filter(field => allowedFields.includes(field.name)), | |
}, | |
]; | |
}, [] as IntospectionResponse['data']['__schema']['types']), | |
}, | |
}, | |
}; | |
} | |
export function middleware(whitelist: Whitelist) { | |
const cache = new LRU<string, any>(10); | |
return function whitelistMiddleware(req: Request, res: Response, next: NextFunction) { | |
const isIntrospection = req.body?.operationName === 'IntrospectionQuery'; | |
if (!isIntrospection) { | |
next(); | |
return; | |
} | |
const { send } = res; | |
// TODO: Prevent infinite recursion | |
let sent = false; | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
res.send = function sendWithWhitelist(body: any) { | |
if (sent) { | |
send.call(this, body); | |
return res; | |
} | |
const hash = createHash('sha256') | |
.update(JSON.stringify(req.body)) | |
.digest('hex'); | |
const cached = cache.get(hash); | |
if (cached !== undefined) { | |
sent = true; | |
send.call(this, cached); | |
return res; | |
} | |
const result = withWhitelist(whitelist, JSON.parse(body)); | |
cache.set(hash, result); | |
sent = true; | |
send.call(this, result); | |
return res; | |
}; | |
next(); | |
}; | |
} |
import { Whitelist } from '.'; | |
export const whitelist: Whitelist = [ | |
{ | |
name: 'Query', | |
fields: [ | |
'recentDeposits', | |
'order', | |
'ordersForSession', | |
'rate', | |
'rates', | |
'session', | |
'affiliateTransfers', | |
'stats', | |
'depositMethods', | |
'settleMethods', | |
'assets', | |
'permissions', | |
'paymentMethodCategories', | |
], | |
}, | |
{ | |
name: 'Mutation', | |
fields: ['createOrder'], | |
}, | |
{ | |
name: 'CacheControlScope', | |
}, | |
{ | |
name: 'Deposit', | |
}, | |
'OwnedOrder', | |
'CreateOrderInput', | |
'JSON', | |
'OwnedDeposit', | |
'Session', | |
'AffiliateTransfer', | |
'Rate', | |
'Stats', | |
'DepositMethod', | |
'SettleMethod', | |
'Asset', | |
'Permissions', | |
'PaymentMethodCategory', | |
]; |
This comment has been minimized.
This comment has been minimized.
A clean and simple solution, I thank you very much for it! The only problem I see is that you need to specific in an explicit list all the nodes you want to show/hide. I'd rather prefer a solution that involves a directive, as an example a @Private or @hidden directive, that can be placed to types and fields. What do you think about it? Is there a way, in your opinion, to change/extend your middleware to handle a GraphQL directive? |
This comment has been minimized.
This comment has been minimized.
Yes, by looking at the declarations from typeDefs. Hoping someone else wants to try first. |
This comment has been minimized.
This comment has been minimized.
Published this as a module with directives support. https://github.com/sideshift/apollo-server-restrict-introspection @mixno86 |
This comment has been minimized.
This comment has been minimized.
Thanks a lot, very very kind! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Start off by allowing a single query and mutation. Open the GraphQL playground and look at the browser console errors for types you missed.