diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index a550a06e7e..d26b72dd69 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -92,7 +92,6 @@ public void ConfigureMvc() { options.EnableEndpointRouting = true; options.Filters.AddService(); - options.Filters.AddService(); options.Filters.AddService(); options.Filters.AddService(); ConfigureMvcOptions?.Invoke(options); @@ -159,7 +158,6 @@ private void AddMiddlewareLayer() _services.AddSingleton(this); _services.TryAddSingleton(); _services.TryAddScoped(); - _services.TryAddScoped(); _services.TryAddScoped(); _services.TryAddScoped(); _services.TryAddSingleton(); diff --git a/src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs deleted file mode 100644 index b42b0ef087..0000000000 --- a/src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - public sealed class AsyncResourceTypeMatchFilter : IAsyncResourceTypeMatchFilter - { - private readonly IResourceContextProvider _provider; - - public AsyncResourceTypeMatchFilter(IResourceContextProvider provider) - { - _provider = provider; - } - - /// - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (next == null) throw new ArgumentNullException(nameof(next)); - - if (context.HttpContext.IsJsonApiRequest() && IsPatchOrPostRequest(context.HttpContext.Request)) - { - var deserializedType = context.ActionArguments.FirstOrDefault().Value?.GetType(); - var targetType = context.ActionDescriptor.Parameters.FirstOrDefault()?.ParameterType; - - if (deserializedType != null && targetType != null && deserializedType != targetType) - { - var resourceFromEndpoint = _provider.GetResourceContext(targetType); - var resourceFromBody = _provider.GetResourceContext(deserializedType); - - throw new ResourceTypeMismatchException(new HttpMethod(context.HttpContext.Request.Method), context.HttpContext.Request.Path, - resourceFromEndpoint, resourceFromBody); - } - } - - await next(); - } - - private static bool IsPatchOrPostRequest(HttpRequest request) - { - return request.Method == "PATCH" || request.Method == "POST"; - } - } -} diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs deleted file mode 100644 index 405c7b30a1..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Verifies the incoming resource type in json:api request body matches the resource type at the current endpoint URL. - /// - public interface IAsyncResourceTypeMatchFilter : IAsyncActionFilter { } -} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 12e8c95bd0..3ec623964d 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -1,11 +1,16 @@ using System; using System.Collections; +using System.Collections.Generic; using System.IO; +using System.Net.Http; +using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; @@ -17,16 +22,19 @@ public class JsonApiReader : IJsonApiReader { private readonly IJsonApiDeserializer _deserializer; private readonly IJsonApiRequest _request; + private readonly IResourceContextProvider _resourceContextProvider; private readonly TraceLogWriter _traceWriter; public JsonApiReader(IJsonApiDeserializer deserializer, IJsonApiRequest request, + IResourceContextProvider resourceContextProvider, ILoggerFactory loggerFactory) { if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); _request = request ?? throw new ArgumentNullException(nameof(request)); + _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); _traceWriter = new TraceLogWriter(loggerFactory); } @@ -63,12 +71,40 @@ public async Task ReadAsync(InputFormatterContext context) ValidatePatchRequestIncludesId(context, model, body); + ValidateIncomingResourceType(context, model); + return await InputFormatterResult.SuccessAsync(model); } + private void ValidateIncomingResourceType(InputFormatterContext context, object model) + { + if (context.HttpContext.IsJsonApiRequest() && IsPatchOrPostRequest(context.HttpContext.Request)) + { + var endpointResourceType = GetEndpointResourceType(); + if (endpointResourceType == null) + { + return; + } + + var bodyResourceTypes = GetBodyResourceTypes(model); + foreach (var bodyResourceType in bodyResourceTypes) + { + if (bodyResourceType != endpointResourceType) + { + var resourceFromEndpoint = _resourceContextProvider.GetResourceContext(endpointResourceType); + var resourceFromBody = _resourceContextProvider.GetResourceContext(bodyResourceType); + + throw new ResourceTypeMismatchException(new HttpMethod(context.HttpContext.Request.Method), + context.HttpContext.Request.Path, + resourceFromEndpoint, resourceFromBody); + } + } + } + } + private void ValidatePatchRequestIncludesId(InputFormatterContext context, object model, string body) { - if (context.HttpContext.Request.Method == "PATCH") + if (context.HttpContext.Request.Method == HttpMethods.Patch) { bool hasMissingId = model is IList list ? HasMissingId(list) : HasMissingId(model); if (hasMissingId) @@ -134,5 +170,27 @@ private async Task GetRequestBody(Stream body) // https://github.com/aspnet/AspNetCore/issues/7644 return await reader.ReadToEndAsync(); } + + private bool IsPatchOrPostRequest(HttpRequest request) + { + return request.Method == HttpMethods.Patch || request.Method == HttpMethods.Post; + } + + private IEnumerable GetBodyResourceTypes(object model) + { + if (model is IEnumerable resourceCollection) + { + return resourceCollection.Select(r => r.GetType()).Distinct(); + } + + return model == null ? new Type[0] : new[] { model.GetType() }; + } + + private Type GetEndpointResourceType() + { + return _request.Kind == EndpointKind.Primary + ? _request.PrimaryResource.ResourceType + : _request.SecondaryResource?.ResourceType; + } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs index 5815d6ec90..46b85a9f97 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs @@ -274,31 +274,6 @@ public async Task CreateResource_SimpleResource_HeaderLocationsAreCorrect() Assert.Equal($"/api/v1/todoItems/{responseItem.Id}", response.Headers.Location.ToString()); } - [Fact] - public async Task CreateResource_ResourceTypeMismatch_IsConflict() - { - // Arrange - string content = JsonConvert.SerializeObject(new - { - data = new - { - type = "people" - } - }); - - // Act - var (body, response) = await Post("/api/v1/todoItems", content); - - // Assert - AssertEqualStatusCode(HttpStatusCode.Conflict, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode); - Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title); - Assert.Equal("Expected resource of type 'todoItems' in POST request body at endpoint '/api/v1/todoItems', instead of 'people'.", errorDocument.Errors[0].Detail); - } - [Fact] public async Task CreateResource_UnknownResourceType_Fails() { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ResourceTypeMismatchTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ResourceTypeMismatchTests.cs new file mode 100644 index 0000000000..141c4ab96c --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ResourceTypeMismatchTests.cs @@ -0,0 +1,108 @@ +using System.Net; +using System.Threading.Tasks; +using JsonApiDotNetCore.Serialization.Objects; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + public sealed class ResourceTypeMismatchTests : FunctionalTestCollection + { + public ResourceTypeMismatchTests(StandardApplicationFactory factory) : base(factory) { } + + [Fact] + public async Task Posting_Resource_With_Mismatching_Resource_Type_Returns_Conflict() + { + // Arrange + string content = JsonConvert.SerializeObject(new + { + data = new + { + type = "people" + } + }); + + // Act + var (body, _) = await Post("/api/v1/todoItems", content); + + // Assert + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode); + Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title); + Assert.Equal("Expected resource of type 'todoItems' in POST request body at endpoint '/api/v1/todoItems', instead of 'people'.", errorDocument.Errors[0].Detail); + } + + [Fact] + public async Task Patching_Resource_With_Mismatching_Resource_Type_Returns_Conflict() + { + // Arrange + string content = JsonConvert.SerializeObject(new + { + data = new + { + type = "people", + id = 1 + } + }); + + // Act + var (body, _) = await Patch("/api/v1/todoItems/1", content); + + // Assert + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode); + Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title); + Assert.Equal("Expected resource of type 'todoItems' in PATCH request body at endpoint '/api/v1/todoItems/1', instead of 'people'.", errorDocument.Errors[0].Detail); + } + + [Fact] + public async Task Patching_Through_Relationship_Link_With_Mismatching_Resource_Type_Returns_Conflict() + { + // Arrange + string content = JsonConvert.SerializeObject(new + { + data = new + { + type = "todoItems", + id = 1 + } + }); + + // Act + var (body, _) = await Patch("/api/v1/todoItems/1/relationships/owner", content); + + // Assert + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode); + Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title); + Assert.Equal("Expected resource of type 'people' in PATCH request body at endpoint '/api/v1/todoItems/1/relationships/owner', instead of 'todoItems'.", errorDocument.Errors[0].Detail); + } + + [Fact] + public async Task Patching_Through_Relationship_Link_With_Multiple_Resources_Types_Returns_Conflict() + { + // Arrange + string content = JsonConvert.SerializeObject(new + { + data = new object[] + { + new { type = "todoItems", id = 1 }, + new { type = "articles", id = 2 }, + } + }); + + // Act + var (body, _) = await Patch("/api/v1/todoItems/1/relationships/childrenTodos", content); + + // Assert + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode); + Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title); + Assert.Equal("Expected resource of type 'todoItems' in PATCH request body at endpoint '/api/v1/todoItems/1/relationships/childrenTodos', instead of 'articles'.", errorDocument.Errors[0].Detail); + } + } +}