From aebdab41624e06cbad20b8d281706f9c7c285930 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 6 May 2026 14:31:03 +0200 Subject: [PATCH 1/2] Add DisableInternalDirectives --- .../Core/src/Types/IReadOnlySchemaOptions.cs | 7 +++++++ src/HotChocolate/Core/src/Types/SchemaOptions.cs | 4 ++++ .../Types/Types/DirectiveType.Initialization.cs | 3 ++- .../v16/migrating/migrate-from-15-to-16.md | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs index bb3678b4e98..3fd38189f9c 100644 --- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs @@ -85,6 +85,13 @@ public interface IReadOnlySchemaOptions /// DirectiveVisibility DefaultDirectiveVisibility { get; } + /// + /// Disables the concept of internal directives so that all directives are treated as public. + /// When set to true, this overrides any explicit Internal() calls on + /// directive types and the setting. + /// + bool DisableInternalDirectives { get; } + /// /// Defines that the default resolver execution strategy. /// diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs index ee7ef7f294c..f48e9f168f2 100644 --- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs @@ -81,6 +81,9 @@ public FieldBindingFlags DefaultFieldBindingFlags public DirectiveVisibility DefaultDirectiveVisibility { get; set; } = DirectiveVisibility.Public; + /// + public bool DisableInternalDirectives { get; set; } + /// public ExecutionStrategy DefaultResolverStrategy { get; set; } = ExecutionStrategy.Parallel; @@ -207,6 +210,7 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options) DefaultBindingBehavior = options.DefaultBindingBehavior, EnableDirectiveIntrospection = options.EnableDirectiveIntrospection, DefaultDirectiveVisibility = options.DefaultDirectiveVisibility, + DisableInternalDirectives = options.DisableInternalDirectives, DefaultResolverStrategy = options.DefaultResolverStrategy, ValidatePipelineOrder = options.ValidatePipelineOrder, StrictRuntimeTypeValidation = options.StrictRuntimeTypeValidation, diff --git a/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs b/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs index 9c265cc0f7f..3d07d5be9bc 100644 --- a/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs +++ b/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs @@ -68,7 +68,8 @@ protected override void OnCompleteType( Locations = configuration.Locations; Arguments = OnCompleteFields(context, configuration); - IsPublic = configuration.IsPublic; + IsPublic = configuration.IsPublic + || context.DescriptorContext.Options.DisableInternalDirectives; Middleware = OnCompleteMiddleware(context, configuration); _createInstance = OnCompleteCreateInstance(context, configuration); diff --git a/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md b/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md index 3428adfa99a..6e47c90149c 100644 --- a/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md +++ b/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md @@ -79,6 +79,20 @@ If you need to access the application service provider from within the schema se IServiceProvider applicationServices = schemaServices.GetRootServiceProvider(); ``` +## Internal directives hidden from schema endpoint + +Previously, the `/graphql/schema.graphql` endpoint was returning the schema containing internal directives like `@authorize`. Starting with v16 the endpoint no longer includes internal directives by default. + +If you need to retain the previous behavior, set `DisableInternalDirectives` to `true` through `ModifyOptions`. This treats every directive as public, even directives that explicitly call `Internal()` and regardless of `DefaultDirectiveVisibility`: + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyOptions(o => o.DisableInternalDirectives = true); +``` + +Be aware that internal directives may carry sensitive information (for example, authorization policies attached via `@authorize`). Only enable this if you understand and accept that risk. + ## Cache size configuration Previously, document and operation cache sizes were globally configured through the `IServiceCollection`. In an effort to align and properly scope our configuration APIs, we've moved the configuration of these caches to the `IRequestExecutorBuilder`. If you're currently calling `AddDocumentCache` or `AddOperationCache` directly on the `IServiceCollection`, move the configuration to `ModifyOptions` on the `IRequestExecutorBuilder`: From 92144d4b885ae9b4630215ff62e1cdde169370ff Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 6 May 2026 14:40:50 +0200 Subject: [PATCH 2/2] Add tests --- .../HttpGetSchemaMiddlewareTests.cs | 26 ++ ...GetSemanticNonNullSchemaMiddlewareTests.cs | 26 ++ ...hen_DisableInternalDirectives_Is_True.snap | 22 ++ ...hen_DisableInternalDirectives_Is_True.snap | 24 ++ .../Execution.Tests/IntrospectionTests.cs | 54 ++++ ...hen_DisableInternalDirectives_Is_True.snap | 271 ++++++++++++++++++ 6 files changed, 423 insertions(+) create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs index 9514b9852a3..0d5c6480b08 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs @@ -281,6 +281,32 @@ public async Task Download_GraphQL_Schema_Does_Not_Include_Internal_Directives() result.MatchSnapshot(); } + [Fact] + public async Task Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True() + { + // arrange + var server = ServerFactory.Create( + services => services + .AddRouting() + .AddGraphQLServer() + .ModifyOptions(o => o.DisableInternalDirectives = true) + .AddDirectiveType() + .AddQueryType(), + app => app + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQLSchema())); + var url = TestServerExtensions.CreateUrl("/graphql/sdl"); + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // act + var response = await server.CreateClient().SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + result.MatchSnapshot(); + } + private sealed class StaticTimeProvider : ITimeProvider { public DateTimeOffset UtcNow { get; } = new(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs index b3d33f8257b..97a96c6aadf 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs @@ -133,6 +133,32 @@ public async Task Download_GraphQL_SemanticNonNull_Schema_Does_Not_Include_Inter result.MatchSnapshot(); } + [Fact] + public async Task Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True() + { + // arrange + var server = ServerFactory.Create( + services => services + .AddRouting() + .AddGraphQLServer() + .ModifyOptions(o => o.DisableInternalDirectives = true) + .AddDirectiveType() + .AddQueryType(), + app => app + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQLSemanticNonNullSchema())); + var url = TestServerExtensions.CreateUrl("/graphql/semantic-non-null-schema.graphql"); + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // act + var response = await server.CreateClient().SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + result.MatchSnapshot(); + } + private sealed class StaticTimeProvider : ITimeProvider { public DateTimeOffset UtcNow { get; } = new(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap new file mode 100644 index 00000000000..3cc948a2566 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap @@ -0,0 +1,22 @@ +schema { + query: Query +} + +type Query { + secret: String! @internal @cost(weight: "10") + public: String! @cost(weight: "10") +} + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost( + "The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." + weight: String! +) on + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | ENUM + | INPUT_FIELD_DEFINITION + +directive @internal on FIELD_DEFINITION diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap new file mode 100644 index 00000000000..4b931794efd --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap @@ -0,0 +1,24 @@ +schema { + query: Query +} + +type Query { + secret: String @internal @cost(weight: "10") @semanticNonNull + public: String @cost(weight: "10") @semanticNonNull +} + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost( + "The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." + weight: String! +) on + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | ENUM + | INPUT_FIELD_DEFINITION + +directive @internal on FIELD_DEFINITION + +directive @semanticNonNull(levels: [Int!] = [0]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs b/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs index 26fbcc00e1c..7376259e088 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs @@ -356,6 +356,60 @@ directive @bar(baz: String, quox: SomeInput) repeatable on FIELD_DEFINITION result.MatchSnapshot(); } + [Fact] + public async Task DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True() + { + var result = + await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString( + """ + type Query { + foo: String + @foo + @bar(baz: "ABC") + @bar(baz: null) + @bar(quox: { a: "ABC" }) + @bar(quox: { a: "DEF" }) + @bar + } + + input SomeInput { + a: String! + } + + directive @bar(baz: String, quox: SomeInput) repeatable on FIELD_DEFINITION + """) + .UseField(next => next) + .ModifyOptions(o => o.EnableDirectiveIntrospection = true) + .ModifyOptions(o => o.DefaultDirectiveVisibility = DirectiveVisibility.Internal) + .ModifyOptions(o => o.DisableInternalDirectives = true) + .AddDirectiveType(new DirectiveType(d => + { + d.Name("foo"); + d.Location(DirectiveLocation.FieldDefinition); + d.Internal(); + })) + .ExecuteRequestAsync( + @"{ + __schema { + types { + fields { + appliedDirectives { + name + args { + name + value + } + } + } + } + } + }"); + + result.MatchSnapshot(); + } + private static Schema CreateSchema() { return SchemaBuilder.New() diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap new file mode 100644 index 00000000000..b3c26b368b0 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap @@ -0,0 +1,271 @@ +{ + "data": { + "__schema": { + "types": [ + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": null + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": null + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": [ + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + }, + { + "appliedDirectives": [] + } + ] + }, + { + "fields": null + }, + { + "fields": null + }, + { + "fields": null + }, + { + "fields": [ + { + "appliedDirectives": [ + { + "name": "foo", + "args": [] + }, + { + "name": "bar", + "args": [ + { + "name": "baz", + "value": "\"ABC\"" + } + ] + }, + { + "name": "bar", + "args": [] + }, + { + "name": "bar", + "args": [ + { + "name": "quox", + "value": "{ a: \"ABC\" }" + } + ] + }, + { + "name": "bar", + "args": [ + { + "name": "quox", + "value": "{ a: \"DEF\" }" + } + ] + }, + { + "name": "bar", + "args": [] + } + ] + } + ] + }, + { + "fields": null + }, + { + "fields": null + }, + { + "fields": null + } + ] + } + } +}