Skip to content

Conditionally allow introspection #569

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

Merged
merged 1 commit into from
Feb 14, 2023
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
75 changes: 75 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package graphql_test
import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/graph-gophers/graphql-go"
Expand Down Expand Up @@ -152,3 +153,77 @@ func ExampleMaxQueryLength() {
// ]
// }
}

func ExampleRestrictIntrospection() {
allowKey := struct{}{}
// only allow introspection if the function below returns true
filter := func(ctx context.Context) bool {
allow, found := ctx.Value(allowKey).(bool)
return found && allow
}
schema := graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{}, graphql.RestrictIntrospection(filter))

query := `{
__type(name: "Episode") {
enumValues {
name
}
}
}`

cases := []struct {
name string
ctx context.Context
}{
{
name: "Empty context",
ctx: context.Background(),
},
{
name: "Introspection forbidden",
ctx: context.WithValue(context.Background(), allowKey, false),
},
{
name: "Introspection allowed",
ctx: context.WithValue(context.Background(), allowKey, true),
},
}
for _, c := range cases {
fmt.Println(c.name, "result:")
res := schema.Exec(c.ctx, query, "", nil)

enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
err := enc.Encode(res)
if err != nil {
panic(err)
}
}
// output:
// Empty context result:
// {
// "data": {}
// }
// Introspection forbidden result:
// {
// "data": {}
// }
// Introspection allowed result:
// {
// "data": {
// "__type": {
// "enumValues": [
// {
// "name": "NEWHOPE"
// },
// {
// "name": "EMPIRE"
// },
// {
// "name": "JEDI"
// }
// ]
// }
// }
// }
}
36 changes: 29 additions & 7 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type Schema struct {
schema *types.Schema
res *resolvable.Schema

allowIntrospection func(ctx context.Context) bool
maxQueryLength int
maxDepth int
maxParallelism int
Expand All @@ -83,7 +84,6 @@ type Schema struct {
logger log.Logger
panicHandler errors.PanicHandler
useStringDescriptions bool
disableIntrospection bool
subscribeResolverTimeout time.Duration
visitors map[string]directives.Visitor
}
Expand Down Expand Up @@ -166,10 +166,32 @@ func PanicHandler(panicHandler errors.PanicHandler) SchemaOpt {
}
}

// DisableIntrospection disables introspection queries.
// RestrictIntrospection accepts a filter func. If this function returns false the introspection is disabled, otherwise it is enabled.
// If this option is not provided the introspection is enabled by default. This option is useful for allowing introspection only to admin users, for example:
//
// filter := func(ctx context.Context) bool {
// u, ok := user.FromContext(ctx)
// return ok && u.IsAdmin()
// }
//
// Do not use it together with [DisableIntrospection], otherwise the option added last takes precedence.
func RestrictIntrospection(fn func(ctx context.Context) bool) SchemaOpt {
return func(s *Schema) {
s.allowIntrospection = fn
}
}

// DisableIntrospection disables introspection queries. This function is left for backwards compatibility reasons and is just a shorthand for:
//
// filter := func(context.Context) bool {
// return false
// }
// graphql.RestrictIntrospection(filter)
//
// Deprecated: use [RestrictIntrospection] filter instead. Do not use it together with [RestrictIntrospection], otherwise the option added last takes precedence.
func DisableIntrospection() SchemaOpt {
return func(s *Schema) {
s.disableIntrospection = true
s.allowIntrospection = func(context.Context) bool { return false }
}
}

Expand Down Expand Up @@ -276,10 +298,10 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str

r := &exec.Request{
Request: selected.Request{
Doc: doc,
Vars: variables,
Schema: s.schema,
DisableIntrospection: s.disableIntrospection,
Doc: doc,
Vars: variables,
Schema: s.schema,
AllowIntrospection: s.allowIntrospection == nil || s.allowIntrospection(ctx), // allow introspection by default, i.e. when allowIntrospection is nil
},
Limiter: make(chan struct{}, s.maxParallelism),
Tracer: s.tracer,
Expand Down
20 changes: 10 additions & 10 deletions internal/exec/selected/selected.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import (
)

type Request struct {
Schema *types.Schema
Doc *types.ExecutableDefinition
Vars map[string]interface{}
Mu sync.Mutex
Errs []*errors.QueryError
DisableIntrospection bool
Schema *types.Schema
Doc *types.ExecutableDefinition
Vars map[string]interface{}
Mu sync.Mutex
Errs []*errors.QueryError
AllowIntrospection bool
}

func (r *Request) AddError(err *errors.QueryError) {
Expand Down Expand Up @@ -80,15 +80,15 @@ func applySelectionSet(r *Request, s *resolvable.Schema, e *resolvable.Object, s

switch field.Name.Name {
case "__typename":
// __typename is available even though r.DisableIntrospection == true
// __typename is available even though r.AllowIntrospection == false
// because it is necessary when using union types and interfaces: https://graphql.org/learn/schema/#union-types
flattenedSels = append(flattenedSels, &TypenameField{
Object: *e,
Alias: field.Alias.Name,
})

case "__schema":
if !r.DisableIntrospection {
if r.AllowIntrospection {
flattenedSels = append(flattenedSels, &SchemaField{
Field: s.Meta.FieldSchema,
Alias: field.Alias.Name,
Expand All @@ -99,7 +99,7 @@ func applySelectionSet(r *Request, s *resolvable.Schema, e *resolvable.Object, s
}

case "__type":
if !r.DisableIntrospection {
if r.AllowIntrospection {
p := packer.ValuePacker{ValueType: reflect.TypeOf("")}
v, err := p.Pack(field.Arguments.MustGet("name").Deserialize(r.Vars))
if err != nil {
Expand All @@ -123,7 +123,7 @@ func applySelectionSet(r *Request, s *resolvable.Schema, e *resolvable.Object, s
}

case "_service":
if !r.DisableIntrospection {
if r.AllowIntrospection {
flattenedSels = append(flattenedSels, &SchemaField{
Field: s.Meta.FieldService,
Alias: field.Alias.Name,
Expand Down