Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 48d8c12

Browse files
author
Bart Koelman
committed
Added input validation for version in URL and request body
1 parent 8c3b097 commit 48d8c12

14 files changed

+1917
-4
lines changed

‎src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

Lines changed: 35 additions & 0 deletions
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;

‎src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs

Lines changed: 2 additions & 0 deletions
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
}

‎src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs

Lines changed: 5 additions & 1 deletion
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;

‎src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs

Lines changed: 3 additions & 0 deletions
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

‎src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs

Lines changed: 41 additions & 3 deletions
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, TyperesourceClrType,
83+
private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, ResourceTyperesourceType,
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)

‎src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs

Lines changed: 10 additions & 0 deletions
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>

‎test/JsonApiDotNetCoreTests/IntegrationTests/OptimisticConcurrency/ConcurrencyDbContext.cs

Lines changed: 1 addition & 0 deletions
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)

‎test/JsonApiDotNetCoreTests/IntegrationTests/OptimisticConcurrency/ConcurrencyFakers.cs

Lines changed: 6 additions & 0 deletions
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
}
Lines changed: 23 additions & 0 deletions
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+
}
Lines changed: 17 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /