Skip to content

Commit 2f0687d

Browse files
author
Bart Koelman
committed
Breaking: Removed access-control action filter attributes such as HttpReadOnly, NoHttpPost etc. because they interfere with relationship endpoints. For example, blocking POST would block creating resources, as well as adding to to-many relationships, which is not very useful. The replacement is to inject just the subset of exposed services, or simply use the Command/Query controllers. When an endpoint is not exposed, we now return HTTP 403 Forbidden instead of 404 or 405.
1 parent 0140070 commit 2f0687d

32 files changed

+1131
-1244
lines changed

docs/usage/extensibility/controllers.md

Lines changed: 25 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -13,83 +13,49 @@ public class ArticlesController : JsonApiController<Article, Guid>
1313
}
1414
```
1515

16+
If you want to setup routes yourself, you can instead inherit from `BaseJsonApiController<TResource, TId>` and override its methods with your own `[HttpGet]`, `[HttpHead]`, `[HttpPost]`, `[HttpPatch]` and `[HttpDelete]` attributes added on them. Don't forget to add `[FromBody]` on parameters where needed.
17+
1618
## Resource Access Control
1719

18-
It is often desirable to limit what methods are exposed on your controller. The first way you can do this, is to simply inherit from `BaseJsonApiController` and explicitly declare what methods are available.
20+
It is often desirable to limit which routes are exposed on your controller.
1921

20-
In this example, if a client attempts to do anything other than GET a resource, an HTTP 404 Not Found response will be returned since no other methods are exposed.
22+
To provide read-only access, inherit from `JsonApiQueryController` instead, which blocks all POST, PATCH and DELETE requests.
23+
Likewise, to provide write-only access, inherit from `JsonApiCommandController`, which blocks all GET and HEAD requests.
2124

22-
This approach is ok, but introduces some boilerplate that can easily be avoided.
25+
You can even make your own mix of allowed routes by calling the alternate constructor of `JsonApiController` and injecting the set of service implementations available.
26+
In some cases, resources may be an aggregation of entities or a view on top of the underlying entities. In these cases, there may not be a writable `IResourceService` implementation, so simply inject the implementation that is available.
2327

2428
```c#
25-
public class ArticlesController : BaseJsonApiController<Article, int>
29+
public class ReportsController : JsonApiController<Report, int>
2630
{
27-
public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
28-
ILoggerFactory loggerFactory, IResourceService<Article, int> resourceService)
29-
: base(options, resourceGraph, loggerFactory, resourceService)
30-
{
31-
}
32-
33-
[HttpGet]
34-
public override async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
35-
{
36-
return await base.GetAsync(cancellationToken);
37-
}
38-
39-
[HttpGet("{id}")]
40-
public override async Task<IActionResult> GetAsync(int id,
41-
CancellationToken cancellationToken)
31+
public ReportsController(IJsonApiOptions options, IResourceGraph resourceGraph,
32+
ILoggerFactory loggerFactory, IGetAllService<Report, int> getAllService)
33+
: base(options, resourceGraph, loggerFactory, getAll: getAllService)
4234
{
43-
return await base.GetAsync(id, cancellationToken);
4435
}
4536
}
4637
```
4738

48-
## Using ActionFilterAttributes
49-
50-
The next option is to use the ActionFilter attributes that ship with the library. The available attributes are:
51-
52-
- `NoHttpPost`: disallow POST requests
53-
- `NoHttpPatch`: disallow PATCH requests
54-
- `NoHttpDelete`: disallow DELETE requests
55-
- `HttpReadOnly`: all of the above
39+
For more information about resource service injection, see [Replacing injected services](~/usage/extensibility/layer-overview.md#replacing-injected-services) and [Resource Services](~/usage/extensibility/services.md).
5640

57-
Not only does this reduce boilerplate, but it also provides a more meaningful HTTP response code.
58-
An attempt to use one of the blacklisted methods will result in a HTTP 405 Method Not Allowed response.
41+
When a route is blocked, an HTTP 403 Forbidden response is returned.
5942

60-
```c#
61-
[HttpReadOnly]
62-
public class ArticlesController : BaseJsonApiController<Article, int>
63-
{
64-
public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
65-
ILoggerFactory loggerFactory, IResourceService<Article, int> resourceService)
66-
: base(options, resourceGraph, loggerFactory, resourceService)
67-
{
68-
}
69-
}
43+
```http
44+
DELETE http://localhost:14140/people/1 HTTP/1.1
7045
```
7146

72-
## Implicit Access By Service Injection
73-
74-
Finally, you can control the allowed methods by supplying only the available service implementations. In some cases, resources may be an aggregation of entities or a view on top of the underlying entities. In these cases, there may not be a writable `IResourceService` implementation, so simply inject the implementation that is available.
75-
76-
As with the ActionFilter attributes, if a service implementation is not available to service a request, HTTP 405 Method Not Allowed will be returned.
77-
78-
For more information about resource service injection, see [Replacing injected services](~/usage/extensibility/layer-overview.md#replacing-injected-services) and [Resource Services](~/usage/extensibility/services.md).
79-
80-
```c#
81-
public class ReportsController : BaseJsonApiController<Report, int>
47+
```json
8248
{
83-
public ReportsController(IJsonApiOptions options, IResourceGraph resourceGraph,
84-
ILoggerFactory loggerFactory, IGetAllService<Report, int> getAllService)
85-
: base(options, resourceGraph, loggerFactory, getAllService)
86-
{
87-
}
88-
89-
[HttpGet]
90-
public override async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
49+
"links": {
50+
"self": "/api/v1/people"
51+
},
52+
"errors": [
9153
{
92-
return await base.GetAsync(cancellationToken);
54+
"id": "dde7f219-2274-4473-97ef-baac3e7c1487",
55+
"status": "403",
56+
"title": "The requested endpoint is not accessible.",
57+
"detail": "Endpoint '/people/1' is not accessible for DELETE requests."
9358
}
59+
]
9460
}
9561
```

docs/usage/extensibility/services.md

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -152,30 +152,16 @@ public class Startup
152152
}
153153
```
154154

155-
Then in the controller, you should inherit from the base controller and pass the services into the named, optional base parameters:
155+
Then in the controller, you should inherit from the JSON:API controller and pass the services into the named, optional base parameters:
156156

157157
```c#
158-
public class ArticlesController : BaseJsonApiController<Article, int>
158+
public class ArticlesController : JsonApiController<Article, int>
159159
{
160160
public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
161161
ILoggerFactory loggerFactory, ICreateService<Article, int> create,
162162
IDeleteService<Article, int> delete)
163163
: base(options, resourceGraph, loggerFactory, create: create, delete: delete)
164164
{
165165
}
166-
167-
[HttpPost]
168-
public override async Task<IActionResult> PostAsync([FromBody] Article resource,
169-
CancellationToken cancellationToken)
170-
{
171-
return await base.PostAsync(resource, cancellationToken);
172-
}
173-
174-
[HttpDelete("{id}")]
175-
public override async Task<IActionResult>DeleteAsync(int id,
176-
CancellationToken cancellationToken)
177-
{
178-
return await base.DeleteAsync(id, cancellationToken);
179-
}
180166
}
181167
```

src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public virtual async Task<IActionResult> GetAsync(CancellationToken cancellation
9595

9696
if (_getAll == null)
9797
{
98-
throw new RequestMethodNotAllowedException(HttpMethod.Get);
98+
throw new RouteNotAvailableException(HttpMethod.Get, Request.Path);
9999
}
100100

101101
IReadOnlyCollection<TResource> resources = await _getAll.GetAsync(cancellationToken);
@@ -115,7 +115,7 @@ public virtual async Task<IActionResult> GetAsync(TId id, CancellationToken canc
115115

116116
if (_getById == null)
117117
{
118-
throw new RequestMethodNotAllowedException(HttpMethod.Get);
118+
throw new RouteNotAvailableException(HttpMethod.Get, Request.Path);
119119
}
120120

121121
TResource resource = await _getById.GetAsync(id, cancellationToken);
@@ -138,7 +138,7 @@ public virtual async Task<IActionResult> GetSecondaryAsync(TId id, string relati
138138

139139
if (_getSecondary == null)
140140
{
141-
throw new RequestMethodNotAllowedException(HttpMethod.Get);
141+
throw new RouteNotAvailableException(HttpMethod.Get, Request.Path);
142142
}
143143

144144
object? rightValue = await _getSecondary.GetSecondaryAsync(id, relationshipName, cancellationToken);
@@ -161,7 +161,7 @@ public virtual async Task<IActionResult> GetRelationshipAsync(TId id, string rel
161161

162162
if (_getRelationship == null)
163163
{
164-
throw new RequestMethodNotAllowedException(HttpMethod.Get);
164+
throw new RouteNotAvailableException(HttpMethod.Get, Request.Path);
165165
}
166166

167167
object? rightValue = await _getRelationship.GetRelationshipAsync(id, relationshipName, cancellationToken);
@@ -183,7 +183,7 @@ public virtual async Task<IActionResult> PostAsync([FromBody] TResource resource
183183

184184
if (_create == null)
185185
{
186-
throw new RequestMethodNotAllowedException(HttpMethod.Post);
186+
throw new RouteNotAvailableException(HttpMethod.Post, Request.Path);
187187
}
188188

189189
if (_options.ValidateModelState && !ModelState.IsValid)
@@ -235,7 +235,7 @@ public virtual async Task<IActionResult> PostRelationshipAsync(TId id, string re
235235

236236
if (_addToRelationship == null)
237237
{
238-
throw new RequestMethodNotAllowedException(HttpMethod.Post);
238+
throw new RouteNotAvailableException(HttpMethod.Post, Request.Path);
239239
}
240240

241241
await _addToRelationship.AddToToManyRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken);
@@ -259,7 +259,7 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource
259259

260260
if (_update == null)
261261
{
262-
throw new RequestMethodNotAllowedException(HttpMethod.Patch);
262+
throw new RouteNotAvailableException(HttpMethod.Patch, Request.Path);
263263
}
264264

265265
if (_options.ValidateModelState && !ModelState.IsValid)
@@ -301,7 +301,7 @@ public virtual async Task<IActionResult> PatchRelationshipAsync(TId id, string r
301301

302302
if (_setRelationship == null)
303303
{
304-
throw new RequestMethodNotAllowedException(HttpMethod.Patch);
304+
throw new RouteNotAvailableException(HttpMethod.Patch, Request.Path);
305305
}
306306

307307
await _setRelationship.SetRelationshipAsync(id, relationshipName, rightValue, cancellationToken);
@@ -321,7 +321,7 @@ public virtual async Task<IActionResult> DeleteAsync(TId id, CancellationToken c
321321

322322
if (_delete == null)
323323
{
324-
throw new RequestMethodNotAllowedException(HttpMethod.Delete);
324+
throw new RouteNotAvailableException(HttpMethod.Delete, Request.Path);
325325
}
326326

327327
await _delete.DeleteAsync(id, cancellationToken);
@@ -359,7 +359,7 @@ public virtual async Task<IActionResult> DeleteRelationshipAsync(TId id, string
359359

360360
if (_removeFromRelationship == null)
361361
{
362-
throw new RequestMethodNotAllowedException(HttpMethod.Delete);
362+
throw new RouteNotAvailableException(HttpMethod.Delete, Request.Path);
363363
}
364364

365365
await _removeFromRelationship.RemoveFromToManyRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken);

src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)