Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
"description": "Allow enabling/disabling GraphQL requests for all entities."
},
"depth-limit": {
"type": "integer",
"type": [ "integer", "null" ],
"description": "Maximum allowed depth of a GraphQL query.",
"default": null
},
Expand Down
17 changes: 16 additions & 1 deletion src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,22 @@ internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar)
}
else if (reader.TokenType is JsonTokenType.Number)
{
graphQLRuntimeOptions = graphQLRuntimeOptions with { DepthLimit = reader.GetInt32(), UserProvidedDepthLimit = true };
int depthLimit;
try
{
depthLimit = reader.GetInt32();
}
catch (JsonException)
{
throw new JsonException($"The 'depth-limit' value must be an integer within the valid range of 1 to Int32.Max or -1.");
}

if (depthLimit < -1 || depthLimit == 0)
{
throw new JsonException($"Invalid depth-limit: {depthLimit}. Specify a depth limit > 0 or remove the existing depth limit by specifying -1.");
}

graphQLRuntimeOptions = graphQLRuntimeOptions with { DepthLimit = depthLimit, UserProvidedDepthLimit = true };
}
else
{
Expand Down
263 changes: 263 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,59 @@ public async Task TestConfigIsValid()
}
}

/// <summary>
/// Test to verify that provided value of depth-limit in the config file should be greater than 0.
/// -1 and null are special values to disable depth limit.
/// This test validates that depth-limit outside the valid range should fail validation
/// during `dab validate` and `dab start`.
/// </summary>
[DataTestMethod]
[DataRow(-1, true, DisplayName = "[PASS]: Valid Value: -1 to disable depth limit")]
[DataRow(0, false, DisplayName = "[FAIL]: Invalid Value: 0 for depth-limit.")]
[DataRow(-2, false, DisplayName = "[FAIL]: Invalid Value: -2 for depth-limit.")]
[DataRow(2, true, DisplayName = "[PASS]: Valid Value: 2 for depth-limit.")]
[DataRow(2147483647, true, DisplayName = "[PASS]: Valid Value: Using Int32.MaxValue(2147483647) for depth-limit.")]
[DataRow(null, true, DisplayName = "[PASS]: Valid Value: null for depth-limit.")]
[TestCategory(TestCategory.MSSQL)]
public async Task TestValidateConfigForDifferentDepthLimit(int? depthLimit, bool isValidInput)
{
//Arrange
// Fetch the MS_SQL integration test config file.
TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT);
const string CUSTOM_CONFIG = "custom-config.json";
FileSystemRuntimeConfigLoader testConfigPath = TestHelper.GetRuntimeConfigLoader();
RuntimeConfig configuration = TestHelper.GetRuntimeConfigProvider(testConfigPath).GetConfig();
configuration = configuration with
{
Runtime = configuration.Runtime with
{
GraphQL = configuration.Runtime.GraphQL with { DepthLimit = depthLimit, UserProvidedDepthLimit = true }
}
};

MockFileSystem fileSystem = new();

// write the content of configuration to the custom-config file and add it to the filesystem.
fileSystem.AddFile(CUSTOM_CONFIG, new MockFileData(configuration.ToJson()));
FileSystemRuntimeConfigLoader configLoader = new(fileSystem);
configLoader.UpdateConfigFilePath(CUSTOM_CONFIG);
RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(configLoader);

Mock<ILogger<RuntimeConfigValidator>> configValidatorLogger = new();
RuntimeConfigValidator configValidator =
new(
configProvider,
fileSystem,
configValidatorLogger.Object,
true);

// Act
bool isSuccess = await configValidator.TryValidateConfig(CUSTOM_CONFIG, TestHelper.ProvisionLoggerFactory());

// Assert
Assert.AreEqual(isValidInput, isSuccess);
}

/// <summary>
/// This test method checks a valid config's entities against
/// the database and ensures they are valid.
Expand Down Expand Up @@ -3711,6 +3764,216 @@ public async Task ValidateNextLinkUsage()
}
}

/// <summary>
/// Tests the enforcement of depth limit restrictions on GraphQL queries and mutations in non-hosted mode.
/// Verifies that requests exceeding the specified depth limit result in a BadRequest,
/// while requests within the limit succeed with the expected status code.
/// Also verifies that the error message contains the current and allowed max depth limit value.
/// </summary>
/// <param name="depthLimit">The maximum allowed depth for GraphQL queries and mutations.</param>
/// <param name="isMutationOperation">Indicates whether the operation is a mutation (true) or a query (false).</param>
/// <param name="expectedStatusCodeForGraphQL">The expected HTTP status code for the operation.</param>
[DataTestMethod]
[DataRow(1, false, HttpStatusCode.BadRequest, DisplayName = "Failed Query execution when max depth limit is set to 1")]
[DataRow(2, false, HttpStatusCode.OK, DisplayName = "Query execution successful when max depth limit is set to 2")]
[DataRow(1, true, HttpStatusCode.BadRequest, DisplayName = "Failed Mutation execution when max depth limit is set to 1")]
[DataRow(2, true, HttpStatusCode.OK, DisplayName = "Mutation execution successful when max depth limit is set to 2")]
[TestCategory(TestCategory.MSSQL)]
public async Task TestDepthLimitRestrictionOnGraphQLInNonHostedMode(
int depthLimit,
bool isMutationOperation,
HttpStatusCode expectedStatusCodeForGraphQL)
{
// Arrange
GraphQLRuntimeOptions graphqlOptions = new(DepthLimit: depthLimit);
graphqlOptions = graphqlOptions with { UserProvidedDepthLimit = true };

DataSource dataSource = new(DatabaseType.MSSQL,
GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null);

RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restOptions: new());
const string CUSTOM_CONFIG = "custom-config.json";
File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson());

string[] args = new[]
{
$"--ConfigFileName={CUSTOM_CONFIG}"
};

using (TestServer server = new(Program.CreateWebHostBuilder(args)))
using (HttpClient client = server.CreateClient())
{
string query;
if (isMutationOperation)
{
// requested mutation operation has depth of 2
query = @"mutation createbook{
createbook(item: { title: ""Book #1"", publisher_id: 1234 }) { // depth: 1
title // depth: 2
publisher_id
}
}";
}
else
{
// requested query operation has depth of 2
query = @"{
book_by_pk(id: 1) { // depth: 1
id, // depth: 2
title,
publisher_id
}
}";
}

object payload = new { query };

HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql")
{
Content = JsonContent.Create(payload)
};

// Act
HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest);

// Assert
Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode);
string body = await graphQLResponse.Content.ReadAsStringAsync();
if (graphQLResponse.StatusCode == HttpStatusCode.OK)
{
Assert.IsFalse(body.Contains("errors"));
}
else
{
Assert.IsTrue(body.Contains("errors"));
Assert.IsTrue(body.Contains($"The GraphQL document has an execution depth of 2 which exceeds the max allowed execution depth of {depthLimit}."));
}
}
}

/// <summary>
/// This Test verfyies that depth-limit specified for graphQL do not affect the introspection queries.
/// In this test, we have specified the depth limit as 2 and we are sending introspection query with depth 6.
/// The expected result is that the query should be successful and should not return any errors.
/// </summary>
[TestCategory(TestCategory.MSSQL)]
[TestMethod]
public async Task TestGraphQLIntrospectionQueriesAreNotImpactedByDepthLimit()
{
// Arrange
GraphQLRuntimeOptions graphqlOptions = new(DepthLimit: 2);
graphqlOptions = graphqlOptions with { UserProvidedDepthLimit = true };

DataSource dataSource = new(DatabaseType.MSSQL,
GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null);

RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restOptions: new());
const string CUSTOM_CONFIG = "custom-config.json";
File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson());

string[] args = new[]
{
$"--ConfigFileName={CUSTOM_CONFIG}"
};

// Act
using (TestServer server = new(Program.CreateWebHostBuilder(args)))
using (HttpClient client = server.CreateClient())
{
// nested depth:6
string query = @"{
__schema { // depth: 1
types { // depth: 2
name
fields {
name // depth: 3
type {
name // depth: 4
kind
ofType { // depth: 5
name // depth: 6
kind
}
}
}
}
}
}";

object payload = new { query };

HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql")
{
Content = JsonContent.Create(payload)
};

HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest);

// Assert
Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode);
string body = await graphQLResponse.Content.ReadAsStringAsync();
Assert.IsFalse(body.Contains("errors"));
}
}

/// <summary>
/// Tests the behavior of GraphQL queries in non-hosted mode when the depth limit is explicitly set to -1 or null.
/// Setting the depth limit to -1 or null is intended to disable the depth limit check, allowing queries of any depth.
/// This test verifies that queries are processed successfully without any errors under these configurations.
/// </summary>
/// <param name="depthLimit"> </param>
[DataTestMethod]
[DataRow(-1, DisplayName = "Setting -1 for depth-limit will disable the depth limit")]
[DataRow(null, DisplayName = "Setting null for depth-limit will disable the depth limit")]
[TestCategory(TestCategory.MSSQL)]
public async Task TestNoDepthLimitOnGrahQLInNonHostedMode(
int? depthLimit)
{
// Arrange
GraphQLRuntimeOptions graphqlOptions = new(DepthLimit: depthLimit);
graphqlOptions = graphqlOptions with { UserProvidedDepthLimit = true };

DataSource dataSource = new(DatabaseType.MSSQL,
GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null);

RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restOptions: new());
const string CUSTOM_CONFIG = "custom-config.json";
File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson());

string[] args = new[]
{
$"--ConfigFileName={CUSTOM_CONFIG}"
};

// Act
using (TestServer server = new(Program.CreateWebHostBuilder(args)))
using (HttpClient client = server.CreateClient())
{
// requested query operation has depth of 2
string query = @"{
book_by_pk(id: 1) { // depth: 1
id, // depth: 2
title,
publisher_id
}
}";

object payload = new { query };

HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql")
{
Content = JsonContent.Create(payload)
};

HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest);

// Assert
Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode);
string body = await graphQLResponse.Content.ReadAsStringAsync();
Assert.IsFalse(body.Contains("errors"));
}
}

/// <summary>
/// Helper function to write custom configuration file. with minimal REST/GraphQL global settings
/// using the supplied entities.
Expand Down
Loading