diff --git a/router/core/graph_server.go b/router/core/graph_server.go index e0ee19adff..cdcadf3666 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -641,6 +641,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context, QueryDepthCache: gm.queryDepthCache, OperationHashCache: gm.operationHashCache, ParseKitPoolSize: s.engineExecutionConfiguration.ParseKitPoolSize, + IntrospectionEnabled: s.Config.introspection, }) operationPlanner := NewOperationPlanner(executor, gm.planCache) diff --git a/router/core/operation_processor.go b/router/core/operation_processor.go index 35c8dd455e..0f77888b0a 100644 --- a/router/core/operation_processor.go +++ b/router/core/operation_processor.go @@ -5,7 +5,6 @@ import ( "context" "crypto/sha256" "fmt" - "github.com/wundergraph/graphql-go-tools/v2/pkg/middleware/operation_complexity" "hash" "io" "net/http" @@ -26,6 +25,7 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/astprinter" "github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient" + "github.com/wundergraph/graphql-go-tools/v2/pkg/middleware/operation_complexity" "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" "github.com/wundergraph/graphql-go-tools/v2/pkg/variablesvalidation" "go.opentelemetry.io/otel/attribute" @@ -93,6 +93,7 @@ type OperationProcessorOptions struct { QueryDepthCache *ristretto.Cache[uint64, int] OperationHashCache *ristretto.Cache[uint64, string] ParseKitPoolSize int + IntrospectionEnabled bool } // OperationProcessor provides shared resources to the parseKit and OperationKit. @@ -104,6 +105,7 @@ type OperationProcessor struct { operationCache *OperationCache parseKits map[int]*parseKit parseKitSemaphore chan int + introspectionEnabled bool } // parseKit is a helper struct to parse, normalize and validate operations @@ -144,6 +146,7 @@ type OperationKit struct { operationProcessor *OperationProcessor kit *parseKit parsedOperation *ParsedOperation + introspectionEnabled bool } type GraphQLRequest struct { @@ -171,6 +174,7 @@ func NewOperationKit(processor *OperationProcessor) *OperationKit { operationDefinitionRef: -1, cache: processor.operationCache, parsedOperation: &ParsedOperation{}, + introspectionEnabled: processor.introspectionEnabled, } } @@ -370,6 +374,72 @@ func (o *OperationKit) FetchPersistedOperation(ctx context.Context, clientInfo * return false, nil } +const ( + schemaIntrospectionFieldName = "__schema" + typeIntrospectionFieldName = "__type" +) + +func (o *OperationKit) isIntrospectionQuery() (result bool, err error) { + var operationDefinitionRef = ast.InvalidRef + var possibleOperationDefinitionRefs = make([]int, 0) + + for i := 0; i < len(o.kit.doc.RootNodes); i++ { + if o.kit.doc.RootNodes[i].Kind == ast.NodeKindOperationDefinition { + possibleOperationDefinitionRefs = append(possibleOperationDefinitionRefs, o.kit.doc.RootNodes[i].Ref) + } + } + + if len(possibleOperationDefinitionRefs) == 0 { + return + } else if len(possibleOperationDefinitionRefs) == 1 { + operationDefinitionRef = possibleOperationDefinitionRefs[0] + } else { + for i := 0; i < len(possibleOperationDefinitionRefs); i++ { + ref := possibleOperationDefinitionRefs[i] + name := o.kit.doc.OperationDefinitionNameString(ref) + + if o.parsedOperation.Request.OperationName == name { + operationDefinitionRef = ref + break + } + } + } + + if operationDefinitionRef == ast.InvalidRef { + return + } + + operationDef := o.kit.doc.OperationDefinitions[operationDefinitionRef] + if operationDef.OperationType != ast.OperationTypeQuery { + return + } + if !operationDef.HasSelections { + return + } + + selectionSet := o.kit.doc.SelectionSets[operationDef.SelectionSet] + if len(selectionSet.SelectionRefs) == 0 { + return + } + + for i := 0; i < len(selectionSet.SelectionRefs); i++ { + selection := o.kit.doc.Selections[selectionSet.SelectionRefs[i]] + if selection.Kind != ast.SelectionKindField { + continue + } + + fieldName := o.kit.doc.FieldNameUnsafeString(selection.Ref) + switch fieldName { + case schemaIntrospectionFieldName, typeIntrospectionFieldName: + continue + default: + return + } + } + + return true, nil +} + // Parse parses the operation, populate the document and set the operation type. // UnmarshalOperationFromBody must be called before calling this method. func (o *OperationKit) Parse() error { @@ -395,6 +465,24 @@ func (o *OperationKit) Parse() error { } } + if !o.introspectionEnabled { + isIntrospection, err := o.isIntrospectionQuery() + + if err != nil { + return &httpGraphqlError{ + message: "could not determine if operation was an introspection query", + statusCode: http.StatusOK, + } + } + + if isIntrospection { + return &httpGraphqlError{ + message: "GraphQL introspection is disabled by Cosmo Router, but the query contained __schema or __type. To enable introspection, set introspection_enabled: true in the Router configuration", + statusCode: http.StatusOK, + } + } + } + for i := range o.kit.doc.RootNodes { if o.kit.doc.RootNodes[i].Kind != ast.NodeKindOperationDefinition { continue @@ -874,6 +962,7 @@ func NewOperationProcessor(opts OperationProcessorOptions) *OperationProcessor { persistedOperationClient: opts.PersistedOperationClient, parseKits: make(map[int]*parseKit, opts.ParseKitPoolSize), parseKitSemaphore: make(chan int, opts.ParseKitPoolSize), + introspectionEnabled: opts.IntrospectionEnabled, } for i := 0; i < opts.ParseKitPoolSize; i++ { processor.parseKitSemaphore <- i diff --git a/router/core/operation_processor_test.go b/router/core/operation_processor_test.go index ed8e1f266d..a2727fcf53 100644 --- a/router/core/operation_processor_test.go +++ b/router/core/operation_processor_test.go @@ -279,3 +279,165 @@ func TestOperationProcessorUnmarshalExtensions(t *testing.T) { }) } } + +const namedIntrospectionQuery = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}` +const singleNamedIntrospectionQueryWithoutOperationName = `{"operationName":"","variables":{},"query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}` +const silentIntrospectionQuery = `{"operationName":null,"variables":{},"query":"{\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}` +const silentIntrospectionQueryWithOperationName = `{"operationName":"IntrospectionQuery","variables":{},"query":"{\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"}` +const schemaIntrospectionQueryWithMultipleQueries = `{"operationName":"IntrospectionQuery","query":"query Hello { world } query IntrospectionQuery { __schema { types { name } } }"}` +const inlineFragmentedIntrospectionQueryType = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery { ... IntrospectionFragment } fragment IntrospectionFragment on Query { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description args { ...InputValue } onOperation onFragment onField } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }"}` +const inlineFragmentedIntrospectionQueryWithFragmentOnQuery = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery { ... on Query { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description args { ...InputValue } onOperation onFragment onField } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }"}` +const fragmentedIntrospectionQuery = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery { ... IntrospectionFragment } fragment IntrospectionFragment on Query { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description args { ...InputValue } onOperation onFragment onField } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }"}` +const typeIntrospectionQueryWithMultipleQueries = `{"operationName":"IntrospectionQuery","query":"query Hello { world } query IntrospectionQuery { __type(name: \"Droid\") { name } }"}` +const typeIntrospectionQuery = `{"operationName":null,"variables":{},"query":"{__type(name:\"Foo\"){kind}}"}` +const nonIntrospectionQuery = `{"operationName":"Foo","query":"query Foo {bar}"}` +const nonIntrospectionQueryWithIntrospectionQueryName = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery {bar}"}` +const nonSchemaIntrospectionQueryWithAliases = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __schema: user { name types: account { balance } } }"}` +const nonTypeIntrospectionQueryWithAliases = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __type: user { name } }"}` +const nonSchemaIntrospectionQueryWithAdditionalFields = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __schema { types { name } } user { name account { balance } } }"}` +const nonTypeIntrospectionQueryWithAdditionalFields = `{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery { __type(name: \"Droid\") { name } user { name account { balance } } }"}` +const nonSchemaIntrospectionQueryWithMultipleQueries = `{"operationName":"Hello","query":"query Hello { world } query IntrospectionQuery { __schema { types { name } } }"}` +const nonTypeIntrospectionQueryWithMultipleQueries = `{"operationName":"Hello","query":"query Hello { world } query IntrospectionQuery { __type(name: \"Droid\") { name } }"}` +const mutationQuery = `{"operationName":null,"query":"mutation Foo {bar}"}` + +func TestOperationProcessorIntrospectionQuery(t *testing.T) { + executor := &Executor{ + PlanConfig: plan.Configuration{}, + RouterSchema: nil, + Resolver: nil, + RenameTypeNames: nil, + } + parser := NewOperationProcessor(OperationProcessorOptions{ + Executor: executor, + MaxOperationSizeInBytes: 10 << 20, + ParseKitPoolSize: 4, + IntrospectionEnabled: false, + }) + testCases := []struct { + Name string + Input string + HttpError bool + Valid bool + }{ + { + Name: "namedIntrospectionQuery", + Input: namedIntrospectionQuery, + HttpError: true, + }, + { + Name: "singleNamedIntrospectionQueryWithoutOperationName", + Input: singleNamedIntrospectionQueryWithoutOperationName, + HttpError: true, + }, + { + Name: "silentIntrospectionQuery", + Input: silentIntrospectionQuery, + HttpError: true, + }, + { + Name: "silentIntrospectionQueryWithOperationName", + Input: silentIntrospectionQueryWithOperationName, + HttpError: true, + }, + { + Name: "schemaIntrospectionQueryWithMultipleQueries", + Input: schemaIntrospectionQueryWithMultipleQueries, + HttpError: true, + }, + { + Name: "inlineFragmentedIntrospectionQueryType", + Input: inlineFragmentedIntrospectionQueryType, + HttpError: true, + }, + { + Name: "inlineFragmentedIntrospectionQueryWithFragmentOnQuery", + Input: inlineFragmentedIntrospectionQueryWithFragmentOnQuery, + HttpError: true, + }, + { + Name: "fragmentedIntrospectionQuery", + Input: fragmentedIntrospectionQuery, + HttpError: true, + }, + { + Name: "typeIntrospectionQueryWithMultipleQueries", + Input: typeIntrospectionQueryWithMultipleQueries, + HttpError: true, + }, + { + Name: "typeIntrospectionQuery", + Input: typeIntrospectionQuery, + HttpError: true, + }, + { + Name: "nonIntrospectionQuery", + Input: nonIntrospectionQuery, + Valid: true, + }, + { + Name: "nonIntrospectionQueryWithIntrospectionQueryName", + Input: nonIntrospectionQueryWithIntrospectionQueryName, + Valid: true, + }, + { + Name: "nonSchemaIntrospectionQueryWithAliases", + Input: nonSchemaIntrospectionQueryWithAliases, + Valid: true, + }, + { + Name: "nonTypeIntrospectionQueryWithAliases", + Input: nonTypeIntrospectionQueryWithAliases, + Valid: true, + }, + { + Name: "nonSchemaIntrospectionQueryWithAdditionalFields", + Input: nonSchemaIntrospectionQueryWithAdditionalFields, + Valid: true, + }, + { + Name: "nonTypeIntrospectionQueryWithAdditionalFields", + Input: nonTypeIntrospectionQueryWithAdditionalFields, + Valid: true, + }, + { + Name: "nonSchemaIntrospectionQueryWithMultipleQueries", + Input: nonSchemaIntrospectionQueryWithMultipleQueries, + Valid: true, + }, + { + Name: "nonTypeIntrospectionQueryWithMultipleQueries", + Input: nonTypeIntrospectionQueryWithMultipleQueries, + Valid: true, + }, + { + Name: "mutationQuery", + Input: mutationQuery, + Valid: true, + }, + } + + var inputError HttpError + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + + kit, err := parser.NewKit() + require.NoError(t, err) + defer kit.Free() + + err = kit.UnmarshalOperationFromBody([]byte(tc.Input)) + assert.NoError(t, err) + + err = kit.Parse() + + if tc.Valid { + assert.NoError(t, err) + } else if tc.HttpError { + assert.True(t, errors.As(err, &inputError), "expected an http error, got %s", err) + assert.Equal(t, err.Error(), "GraphQL introspection is disabled by Cosmo Router, but the query contained __schema or __type. To enable introspection, set introspection_enabled: true in the Router configuration") + } else { + assert.Error(t, err) + } + }) + } +}