Skip to content

Commit fd03010

Browse files
author
Bart Koelman
committed
Added input validation for version in URL and request body
1 parent 73f8832 commit fd03010

14 files changed

+1917
-4
lines changed

Diff for: src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

+35
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
6767

6868
SetupResourceRequest((JsonApiRequest)request, primaryResourceType, routeValues, httpContext.Request);
6969

70+
if (!await ValidateVersionAsync(request, httpContext, options.SerializerWriteOptions))
71+
{
72+
return;
73+
}
74+
7075
httpContext.RegisterJsonApiRequest();
7176
}
7277
else if (IsRouteForOperations(routeValues))
@@ -206,6 +211,36 @@ private static async Task<bool> ValidateAcceptHeaderAsync(MediaTypeHeaderValue a
206211
return true;
207212
}
208213

214+
private static async Task<bool> ValidateVersionAsync(IJsonApiRequest request, HttpContext httpContext, JsonSerializerOptions serializerOptions)
215+
{
216+
if (!request.IsReadOnly)
217+
{
218+
if (request.PrimaryResourceType!.IsVersioned && request.WriteOperation != WriteOperationKind.CreateResource && request.PrimaryVersion == null)
219+
{
220+
await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.BadRequest)
221+
{
222+
Title = "The 'version' parameter is required at this endpoint.",
223+
Detail = $"Resources of type '{request.PrimaryResourceType.PublicName}' require the version to be specified."
224+
});
225+
226+
return false;
227+
}
228+
229+
if (!request.PrimaryResourceType.IsVersioned && request.PrimaryVersion != null)
230+
{
231+
await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.BadRequest)
232+
{
233+
Title = "The 'version' parameter is not supported at this endpoint.",
234+
Detail = $"Resources of type '{request.PrimaryResourceType.PublicName}' are not versioned."
235+
});
236+
237+
return false;
238+
}
239+
}
240+
241+
return true;
242+
}
243+
209244
private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error)
210245
{
211246
httpResponse.ContentType = HeaderConstants.MediaType;

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs

+2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ private static ResourceIdentityRequirements CreateDataRequirements(AtomicReferen
145145
IdConstraint = refRequirements.IdConstraint,
146146
IdValue = refResult.Resource.StringId,
147147
LidValue = refResult.Resource.LocalId,
148+
VersionConstraint = !refResult.ResourceType.IsVersioned ? JsonElementConstraint.Forbidden : null,
149+
VersionValue = refResult.Resource.GetVersion(),
148150
RelationshipName = refResult.Relationship?.PublicName
149151
};
150152
}

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ private ResourceIdentityRequirements CreateIdentityRequirements(RequestAdapterSt
6868
{
6969
ResourceType = state.Request.PrimaryResourceType,
7070
IdConstraint = idConstraint,
71-
IdValue = state.Request.PrimaryId
71+
IdValue = state.Request.PrimaryId,
72+
VersionConstraint = state.Request.PrimaryResourceType!.IsVersioned && state.Request.WriteOperation != WriteOperationKind.CreateResource
73+
? JsonElementConstraint.Required
74+
: JsonElementConstraint.Forbidden,
75+
VersionValue = state.Request.PrimaryVersion
7276
};
7377

7478
return requirements;

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using JsonApiDotNetCore.Middleware;
56
using JsonApiDotNetCore.Resources;
67
using JsonApiDotNetCore.Resources.Annotations;
78
using JsonApiDotNetCore.Serialization.Objects;
@@ -76,6 +77,8 @@ private static SingleOrManyData<ResourceIdentifierObject> ToIdentifierData(Singl
7677
{
7778
ResourceType = relationship.RightType,
7879
IdConstraint = JsonElementConstraint.Required,
80+
VersionConstraint = !relationship.RightType.IsVersioned ? JsonElementConstraint.Forbidden :
81+
state.Request.Kind == EndpointKind.AtomicOperations ? null : JsonElementConstraint.Required,
7982
RelationshipName = relationship.PublicName
8083
};
8184

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs

+41-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory
3434
ArgumentGuard.NotNull(state, nameof(state));
3535

3636
ResourceType resourceType = ResolveType(identity, requirements, state);
37-
IIdentifiable resource = CreateResource(identity, requirements, resourceType.ClrType, state);
37+
IIdentifiable resource = CreateResource(identity, requirements, resourceType, state);
3838

3939
return (resource, resourceType);
4040
}
@@ -80,7 +80,7 @@ private static void AssertIsCompatibleResourceType(ResourceType actual, Resource
8080
}
8181
}
8282

83-
private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceClrType,
83+
private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, ResourceType resourceType,
8484
RequestAdapterState state)
8585
{
8686
if (state.Request.Kind != EndpointKind.AtomicOperations)
@@ -99,10 +99,20 @@ private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentit
9999
AssertHasNoId(identity, state);
100100
}
101101

102+
if (requirements.VersionConstraint == JsonElementConstraint.Required)
103+
{
104+
AssertHasVersion(identity, state);
105+
}
106+
else if (!resourceType.IsVersioned || requirements.VersionConstraint == JsonElementConstraint.Forbidden)
107+
{
108+
AssertHasNoVersion(identity, state);
109+
}
110+
102111
AssertSameIdValue(identity, requirements.IdValue, state);
103112
AssertSameLidValue(identity, requirements.LidValue, state);
113+
AssertSameVersionValue(identity, requirements.VersionValue, state);
104114

105-
IIdentifiable resource = _resourceFactory.CreateInstance(resourceClrType);
115+
IIdentifiable resource = _resourceFactory.CreateInstance(resourceType.ClrType);
106116
AssignStringId(identity, resource, state);
107117
resource.LocalId = identity.Lid;
108118
resource.SetVersion(identity.Version);
@@ -159,6 +169,23 @@ private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterStat
159169
}
160170
}
161171

172+
private static void AssertHasVersion(IResourceIdentity identity, RequestAdapterState state)
173+
{
174+
if (identity.Version == null)
175+
{
176+
throw new ModelConversionException(state.Position, "The 'version' element is required.", null);
177+
}
178+
}
179+
180+
private static void AssertHasNoVersion(IResourceIdentity identity, RequestAdapterState state)
181+
{
182+
if (identity.Version != null)
183+
{
184+
using IDisposable _ = state.Position.PushElement("version");
185+
throw new ModelConversionException(state.Position, "Unexpected 'version' element.", null);
186+
}
187+
}
188+
162189
private static void AssertSameIdValue(IResourceIdentity identity, string? expected, RequestAdapterState state)
163190
{
164191
if (expected != null && identity.Id != expected)
@@ -181,6 +208,17 @@ private static void AssertSameLidValue(IResourceIdentity identity, string? expec
181208
}
182209
}
183210

211+
private static void AssertSameVersionValue(IResourceIdentity identity, string? expected, RequestAdapterState state)
212+
{
213+
if (expected != null && identity.Version != expected)
214+
{
215+
using IDisposable _ = state.Position.PushElement("version");
216+
217+
throw new ModelConversionException(state.Position, "Conflicting 'version' values found.",
218+
$"Expected '{expected}' instead of '{identity.Version}'.", HttpStatusCode.Conflict);
219+
}
220+
}
221+
184222
private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state)
185223
{
186224
if (identity.Id != null)

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs

+10
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ public sealed class ResourceIdentityRequirements
3030
/// </summary>
3131
public string? LidValue { get; init; }
3232

33+
/// <summary>
34+
/// When not null, indicates the presence or absence of the "version" element.
35+
/// </summary>
36+
public JsonElementConstraint? VersionConstraint { get; init; }
37+
38+
/// <summary>
39+
/// When not null, indicates what the value of the "version" element must be.
40+
/// </summary>
41+
public string? VersionValue { get; init; }
42+
3343
/// <summary>
3444
/// When not null, indicates the name of the relationship to use in error messages.
3545
/// </summary>

Diff for: test/JsonApiDotNetCoreTests/IntegrationTests/OptimisticConcurrency/ConcurrencyDbContext.cs

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class ConcurrencyDbContext : DbContext
1515
public DbSet<WebImage> WebImages => Set<WebImage>();
1616
public DbSet<PageFooter> PageFooters => Set<PageFooter>();
1717
public DbSet<WebLink> WebLinks => Set<WebLink>();
18+
public DbSet<DeploymentJob> DeploymentJobs => Set<DeploymentJob>();
1819

1920
public ConcurrencyDbContext(DbContextOptions<ConcurrencyDbContext> options)
2021
: base(options)

Diff for: test/JsonApiDotNetCoreTests/IntegrationTests/OptimisticConcurrency/ConcurrencyFakers.cs

+6
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,18 @@ internal sealed class ConcurrencyFakers : FakerContainer
4848
.RuleFor(webLink => webLink.Url, faker => faker.Internet.Url())
4949
.RuleFor(webLink => webLink.OpensInNewTab, faker => faker.Random.Bool()));
5050

51+
private readonly Lazy<Faker<DeploymentJob>> _lazyDeploymentJobFaker = new(() =>
52+
new Faker<DeploymentJob>()
53+
.UseSeed(GetFakerSeed())
54+
.RuleFor(deploymentJob => deploymentJob.StartedAt, faker => faker.Date.PastOffset()));
55+
5156
public Faker<WebPage> WebPage => _lazyWebPageFaker.Value;
5257
public Faker<FriendlyUrl> FriendlyUrl => _lazyFriendlyUrlFaker.Value;
5358
public Faker<TextBlock> TextBlock => _lazyTextBlockFaker.Value;
5459
public Faker<Paragraph> Paragraph => _lazyParagraphFaker.Value;
5560
public Faker<WebImage> WebImage => _lazyWebImageFaker.Value;
5661
public Faker<PageFooter> PageFooter => _lazyPageFooterFaker.Value;
5762
public Faker<WebLink> WebLink => _lazyWebLinkFaker.Value;
63+
public Faker<DeploymentJob> DeploymentJob => _lazyDeploymentJobFaker.Value;
5864
}
5965
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.DataAnnotations;
4+
using JetBrains.Annotations;
5+
using JsonApiDotNetCore.Resources;
6+
using JsonApiDotNetCore.Resources.Annotations;
7+
8+
namespace JsonApiDotNetCoreTests.IntegrationTests.OptimisticConcurrency
9+
{
10+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
11+
public sealed class DeploymentJob : Identifiable<Guid>
12+
{
13+
[Attr]
14+
[Required]
15+
public DateTimeOffset? StartedAt { get; set; }
16+
17+
[HasOne]
18+
public DeploymentJob? ParentJob { get; set; }
19+
20+
[HasMany]
21+
public IList<DeploymentJob> ChildJobs { get; set; } = new List<DeploymentJob>();
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Services;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace JsonApiDotNetCoreTests.IntegrationTests.OptimisticConcurrency
8+
{
9+
public sealed class DeploymentJobsController : JsonApiController<DeploymentJob, Guid>
10+
{
11+
public DeploymentJobsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
12+
IResourceService<DeploymentJob, Guid> resourceService)
13+
: base(options, resourceGraph, loggerFactory, resourceService)
14+
{
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)