Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: disable introspection plugin #2231

Merged
merged 5 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/graphql-yoga-2231-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'graphql-yoga': patch
---
dependencies updates:
- Updated dependency [`@envelop/parser-cache@^5.0.4` ↗︎](https://www.npmjs.com/package/@envelop/parser-cache/v/5.0.4) (from `5.0.4`, in `dependencies`)
- Updated dependency [`@envelop/validation-cache@^5.0.5` ↗︎](https://www.npmjs.com/package/@envelop/validation-cache/v/5.0.5) (from `5.0.4`, in `dependencies`)
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,17 @@
"weak-napi": "2.0.2",
"wrangler": "2.6.1"
},
"resolutions": {
"graphql": "16.6.0",
"@envelop/core": "3.0.4",
"@changesets/assemble-release-plan": "5.2.1"
},
"pnpm": {
"patchedDependencies": {
"@changesets/assemble-release-plan@5.2.1": "patches/@changesets__assemble-release-plan@5.2.1.patch",
"@graphiql/react@0.13.3": "patches/@graphiql__react@0.13.3.patch",
"formdata-node@4.4.1": "patches/formdata-node@4.4.1.patch",
"nextra-theme-docs@2.0.0-beta.43": "patches/nextra-theme-docs@2.0.0-beta.43.patch"
},
"overrides": {
"graphql": "16.6.0",
"@envelop/core": "3.0.4",
"@changesets/assemble-release-plan": "5.2.1"
}
}
}
4 changes: 2 additions & 2 deletions packages/graphql-yoga/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
},
"dependencies": {
"@envelop/core": "3.0.4",
"@envelop/parser-cache": "5.0.4",
"@envelop/validation-cache": "5.0.4",
"@envelop/parser-cache": "^5.0.4",
"@envelop/validation-cache": "^5.0.5",
"@graphql-tools/executor": "0.0.9",
"@graphql-tools/schema": "^9.0.0",
"@graphql-tools/utils": "^9.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection'
import { createYoga, createSchema } from 'graphql-yoga'

describe('disable introspection', () => {
test('can disable introspection', async () => {
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
_: Boolean
}
`,
})

const yoga = createYoga({ schema, plugins: [useDisableIntrospection()] })

const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query: `{ __schema { types { name } } }` }),
})

expect(response.status).toEqual(200)
const result = await response.json()
expect(result.data).toEqual(undefined)
expect(result.errors).toHaveLength(2)
})

test('can disable introspection conditionally', async () => {
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
_: Boolean
}
`,
})

const yoga = createYoga({
schema,
plugins: [
useDisableIntrospection({
isDisabled: () => true,
}),
],
})

const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query: `{ __schema { types { name } } }` }),
})

expect(response.status).toEqual(200)
const result = await response.json()
expect(result.data).toEqual(undefined)
expect(result.errors).toHaveLength(2)
})

test('can disable introspection based on headers', async () => {
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
_: Boolean
}
`,
})

const yoga = createYoga({
schema,
plugins: [
useDisableIntrospection({
isDisabled: (request) =>
request.headers.get('x-disable-introspection') === '1',
}),
],
// uncomment this and the tests will pass
// validationCache: false,
})

// First request uses the header to disable introspection
let response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-disable-introspection': '1',
},
body: JSON.stringify({ query: `{ __schema { types { name } } }` }),
})

expect(response.status).toEqual(200)
let result = await response.json()
expect(result.data).toEqual(undefined)
expect(result.errors).toHaveLength(2)

// Seconds request does not disable introspection
response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ query: `{ __schema { types { name } } }` }),
})

expect(response.status).toEqual(200)
result = await response.json()
expect(result.data).toBeDefined()
expect(result.errors).toBeUndefined()
})
})
50 changes: 50 additions & 0 deletions packages/plugins/disable-introspection/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@graphql-yoga/plugin-disable-introspection",
"version": "0.0.0",
"description": "Disable Introspection plugin for GraphQL Yoga.",
"repository": {
"type": "git",
"url": "https://github.com/dotansimha/graphql-yoga.git",
"directory": "packages/plugins/disable-introspection"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"scripts": {
"check": "tsc --pretty --noEmit"
},
"author": "Laurin Quast <laurinquast@googlemail.com>",
"license": "MIT",
"exports": {
".": {
"require": {
"types": "./dist/typings/index.d.cts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
},
"default": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./package.json": "./package.json"
},
"typings": "dist/typings/index.d.ts",
"typescript": {
"definition": "dist/typings/index.d.ts"
},
"publishConfig": {
"directory": "dist",
"access": "public"
},
"peerDependencies": {
"graphql-yoga": "^3.1.1",
"graphql": "^15.2.0 || ^16.0.0"
},
"devDependencies": {
"graphql-yoga": "workspace:*"
},
"type": "module"
}
27 changes: 27 additions & 0 deletions packages/plugins/disable-introspection/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Plugin, PromiseOrValue } from 'graphql-yoga'
import { NoSchemaIntrospectionCustomRule } from 'graphql'

type UseDisableIntrospectionArgs = {
isDisabled?: (request: Request) => PromiseOrValue<boolean>
}

const store = new WeakMap<Request, boolean>()

export const useDisableIntrospection = (
props?: UseDisableIntrospectionArgs,
): Plugin => {
return {
async onRequest({ request }) {
const isDisabled = props?.isDisabled
? await props.isDisabled(request)
: true
store.set(request, isDisabled)
},
onValidate({ addValidationRule, context }) {
const isDisabled = store.get(context.request) ?? true
if (isDisabled) {
addValidationRule(NoSchemaIntrospectionCustomRule)
}
},
}
}
17 changes: 12 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 28 additions & 1 deletion website/src/pages/docs/features/introspection.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ GraphQL schema introspection is also a feature that allows clients to ask a Grap

```ts "Disabling GraphQL schema introspection with a plugin" {7}
import { createYoga } from 'graphql-yoga'
import { useDisableIntrospection } from '@envelop/disable-introspection'
import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection'

// Provide your schema
const yoga = createYoga({
Expand All @@ -34,6 +34,33 @@ server.listen(4000, () => {
})
```

## Disable Introspection based on the GraphQL Request

Somnetimes you want to allow introspectition for certain users.
You can access the `Request` object and determine based on that whether introspection should be enabled or not.
E.g. you can check the headers.

```ts "Disabling GraphQL schema introspection conditionally" {7}
import { createYoga } from 'graphql-yoga'
import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection'

// Provide your schema
const yoga = createYoga({
graphiql: false,
plugins: [
useDisableIntrospection({
isDisabled: (request) =>
request.headers.get('x-allow-introspection') !== 'secret-access-key'
})
]
})

const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
```

## Disabling Field Suggestions

When executing invalid GraphQL operation the GraphQL engine will try to construct smart suggestions that hint typos in the executed GraphQL document.
Expand Down