Skip to content

Commit d5c57cb

Browse files
author
Bart Koelman
committed
Added options.MaximumIncludeDepth
1 parent 41df412 commit d5c57cb

File tree

6 files changed

+103
-5
lines changed

6 files changed

+103
-5
lines changed

docs/usage/options.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,20 @@ options.UseRelativeLinks = true;
6565
## Unknown Query String Parameters
6666

6767
If you would like to use unknown query string parameters (parameters not reserved by the json:api specification or registered using ResourceDefinitions), you can set `AllowUnknownQueryStringParameters = true`.
68-
When set, an HTTP 400 Bad Request for unknown query string parameters.
68+
When set, an HTTP 400 Bad Request is returned for unknown query string parameters.
6969

7070
```c#
7171
options.AllowUnknownQueryStringParameters = true;
7272
```
7373

74+
## Maximum include depth
75+
76+
To limit the maximum depth of nested includes, use `MaximumIncludeDepth`. This is null by default, which means unconstrained. If set and a request exceeds the limit, an HTTP 400 Bad Request is returned.
77+
78+
```c#
79+
options.MaximumIncludeDepth = 1;
80+
```
81+
7482
## Custom Serializer Settings
7583

7684
We use Newtonsoft.Json for all serialization needs.
@@ -82,6 +90,8 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter());
8290
options.SerializerSettings.Formatting = Formatting.Indented;
8391
```
8492

93+
Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored.
94+
8595
## Enable ModelState Validation
8696

8797
If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState = true`. By default, no model validation is performed.

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,12 @@ public interface IJsonApiOptions
163163
/// </summary>
164164
bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; }
165165

166-
// TODO: Add MaximumIncludeDepth
166+
/// <summary>
167+
/// Controls how many levels deep includes are allowed to be nested.
168+
/// For example, MaximumIncludeDepth=1 would allow ?include=articles but not ?include=articles.revisions.
169+
/// <c>null</c> by default, which means unconstrained.
170+
/// </summary>
171+
int? MaximumIncludeDepth { get; }
167172

168173
/// <summary>
169174
/// Specifies the settings that are used by the <see cref="JsonSerializer"/>.

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public class JsonApiOptions : IJsonApiOptions
6868
/// <inheritdoc/>
6969
public bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; }
7070

71+
public int? MaximumIncludeDepth { get; set; }
72+
7173
/// <inheritdoc/>
7274
public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings
7375
{

src/JsonApiDotNetCore/Internal/QueryStrings/IncludeQueryStringParameterReader.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Collections.Generic;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Configuration;
24
using JsonApiDotNetCore.Controllers;
35
using JsonApiDotNetCore.Exceptions;
46
using JsonApiDotNetCore.Internal.Contracts;
@@ -20,12 +22,14 @@ public interface IIncludeQueryStringParameterReader : IQueryStringParameterReade
2022

2123
public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIncludeQueryStringParameterReader
2224
{
25+
private readonly IJsonApiOptions _options;
2326
private IncludeExpression _includeExpression;
2427
private string _lastParameterName;
2528

26-
public IncludeQueryStringParameterReader(ICurrentRequest currentRequest, IResourceContextProvider resourceContextProvider)
29+
public IncludeQueryStringParameterReader(ICurrentRequest currentRequest, IResourceContextProvider resourceContextProvider, IJsonApiOptions options)
2730
: base(currentRequest, resourceContextProvider)
2831
{
32+
_options = options;
2933
}
3034

3135
public bool IsEnabled(DisableQueryAttribute disableQueryAttribute)
@@ -58,7 +62,31 @@ private IncludeExpression GetInclude(string parameterValue)
5862
var parser = new IncludeParser(parameterValue,
5963
(path, _) => ChainResolver.ResolveRelationshipChain(RequestResource, path, ValidateInclude));
6064

61-
return parser.Parse();
65+
IncludeExpression include = parser.Parse();
66+
67+
ValidateMaximumIncludeDepth(include);
68+
69+
return include;
70+
}
71+
72+
private void ValidateMaximumIncludeDepth(IncludeExpression include)
73+
{
74+
if (_options.MaximumIncludeDepth != null)
75+
{
76+
var chains = IncludeChainConverter.GetRelationshipChains(include);
77+
78+
foreach (var chain in chains)
79+
{
80+
if (chain.Fields.Count > _options.MaximumIncludeDepth)
81+
{
82+
var path = string.Join('.', chain.Fields.Select(field => field.PublicName));
83+
84+
throw new InvalidQueryStringParameterException(_lastParameterName,
85+
"Including at the requested depth is not allowed.",
86+
$"Including '{path}' exceeds the maximum inclusion depth of {_options.MaximumIncludeDepth}.");
87+
}
88+
}
89+
}
6290
}
6391

6492
private void ValidateInclude(RelationshipAttribute relationship, ResourceContext resourceContext, string path)

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Includes/IncludeTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Threading.Tasks;
55
using FluentAssertions;
66
using FluentAssertions.Extensions;
7+
using JsonApiDotNetCore;
8+
using JsonApiDotNetCore.Configuration;
79
using JsonApiDotNetCore.Models;
810
using JsonApiDotNetCore.Models.JsonApiDocuments;
911
using JsonApiDotNetCore.Services;
@@ -27,6 +29,9 @@ public IncludeTests(IntegrationTestContext<Startup, AppDbContext> testContext)
2729
{
2830
services.AddScoped<IResourceService<Article>, JsonApiResourceService<Article>>();
2931
});
32+
33+
var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
34+
options.MaximumIncludeDepth = null;
3035
}
3136

3237
[Fact]
@@ -786,5 +791,52 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
786791
responseDocument.Included[0].Id.Should().Be(todoItems[0].Owner.StringId);
787792
responseDocument.Included[0].Attributes["firstName"].Should().Be(todoItems[0].Owner.FirstName);
788793
}
794+
795+
[Fact]
796+
public async Task Can_include_at_configured_maximum_inclusion_depth()
797+
{
798+
// Arrange
799+
var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
800+
options.MaximumIncludeDepth = 1;
801+
802+
var blog = new Blog();
803+
804+
await _testContext.RunOnDatabaseAsync(async dbContext =>
805+
{
806+
dbContext.Blogs.Add(blog);
807+
808+
await dbContext.SaveChangesAsync();
809+
});
810+
811+
var route = $"/api/v1/blogs/{blog.StringId}/articles?include=author,revisions";
812+
813+
// Act
814+
var (httpResponse, _) = await _testContext.ExecuteGetAsync<Document>(route);
815+
816+
// Assert
817+
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
818+
}
819+
820+
[Fact]
821+
public async Task Cannot_exceed_configured_maximum_inclusion_depth()
822+
{
823+
// Arrange
824+
var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
825+
options.MaximumIncludeDepth = 1;
826+
827+
var route = "/api/v1/blogs/123/owner?include=articles.revisions";
828+
829+
// Act
830+
var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync<ErrorDocument>(route);
831+
832+
// Assert
833+
httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
834+
835+
responseDocument.Errors.Should().HaveCount(1);
836+
responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
837+
responseDocument.Errors[0].Title.Should().Be("Including at the requested depth is not allowed.");
838+
responseDocument.Errors[0].Detail.Should().Be("Including 'articles.revisions' exceeds the maximum inclusion depth of 1.");
839+
responseDocument.Errors[0].Source.Parameter.Should().Be("include");
840+
}
789841
}
790842
}

test/UnitTests/QueryStringParameters/IncludeParseTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Linq;
33
using System.Net;
44
using FluentAssertions;
5+
using JsonApiDotNetCore.Configuration;
56
using JsonApiDotNetCore.Controllers;
67
using JsonApiDotNetCore.Exceptions;
78
using JsonApiDotNetCore.Internal.QueryStrings;
@@ -15,7 +16,7 @@ public sealed class IncludeParseTests : ParseTestsBase
1516

1617
public IncludeParseTests()
1718
{
18-
_reader = new IncludeQueryStringParameterReader(CurrentRequest, ResourceGraph);
19+
_reader = new IncludeQueryStringParameterReader(CurrentRequest, ResourceGraph, new JsonApiOptions());
1920
}
2021

2122
[Theory]

0 commit comments

Comments
 (0)