diff --git a/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs b/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs new file mode 100644 index 0000000000..3ef96c26f5 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Controllers +{ + public abstract class HttpRestrictAttribute : ActionFilterAttribute, IAsyncActionFilter + { + protected abstract string[] Methods { get; } + + public override async Task OnActionExecutionAsync( + ActionExecutingContext context, + ActionExecutionDelegate next) + { + var method = context.HttpContext.Request.Method; + + if(CanExecuteAction(method) == false) + throw new JsonApiException("405", $"This resource does not support {method} requests."); + + await next(); + } + + private bool CanExecuteAction(string requestMethod) + { + return Methods.Contains(requestMethod) == false; + } + } + + public class HttpReadOnlyAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "POST", "PATCH", "DELETE" }; + } + + public class NoHttpPostAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "POST" }; + } + + public class NoHttpPatchAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "PATCH" }; + } + + public class NoHttpDeleteAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "DELETE" }; + } +} diff --git a/src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs b/src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs new file mode 100644 index 0000000000..57fafc8fc6 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs @@ -0,0 +1,73 @@ +using JsonApiDotNetCore.Controllers; +using Microsoft.AspNetCore.Mvc; + +namespace JsonApiDotNetCoreExample.Controllers.Restricted +{ + [Route("[controller]")] + [HttpReadOnly] + public class ReadOnlyController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } + + [Route("[controller]")] + [NoHttpPost] + public class NoHttpPostController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } + + [Route("[controller]")] + [NoHttpPatch] + public class NoHttpPatchController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } + + [Route("[controller]")] + [NoHttpDelete] + public class NoHttpDeleteController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs new file mode 100644 index 0000000000..90496b3690 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class HttpReadOnlyTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "readonly"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_POST_Requests() + { + // arrange + const string route = "readonly"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Rejects_PATCH_Requests() + { + // arrange + const string route = "readonly"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Rejects_DELETE_Requests() + { + // arrange + const string route = "readonly"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs new file mode 100644 index 0000000000..32e7eaf109 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class nohttpdeleteTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_POST_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_PATCH_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_DELETE_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs new file mode 100644 index 0000000000..5b8a33f16a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class nohttppatchTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_POST_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_PATCH_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Allows_DELETE_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs new file mode 100644 index 0000000000..f68a65a037 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class NoHttpPostTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_POST_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Allows_PATCH_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_DELETE_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file