Created
May 1, 2020 13:07
-
-
Save abrkn/2edb028da330a875c1598250a1a8d8e5 to your computer and use it in GitHub Desktop.
Whitelist GraphQL introspection types returned by apollo-server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | |
]; |
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?
Yes, by looking at the declarations from typeDefs. Hoping someone else wants to try first.
Published this as a module with directives support. https://github.com/sideshift/apollo-server-restrict-introspection
@mixno86
Thanks a lot, very very kind!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Start off by allowing a single query and mutation. Open the GraphQL playground and look at the browser console errors for types you missed.