1+ using System . Net ;
12using JetBrains . Annotations ;
23using JsonApiDotNetCore . AtomicOperations ;
34using JsonApiDotNetCore . Configuration ;
45using JsonApiDotNetCore . Errors ;
56using JsonApiDotNetCore . Middleware ;
67using JsonApiDotNetCore . Resources ;
8+ using JsonApiDotNetCore . Serialization . Objects ;
79using Microsoft . AspNetCore . Mvc ;
810using Microsoft . AspNetCore . Mvc . ModelBinding ;
911using Microsoft . Extensions . Logging ;
@@ -22,23 +24,26 @@ public abstract class BaseJsonApiOperationsController : CoreJsonApiController
2224 private readonly IOperationsProcessor _processor ;
2325 private readonly IJsonApiRequest _request ;
2426 private readonly ITargetedFields _targetedFields ;
27+ private readonly IAtomicOperationFilter _operationFilter ;
2528 private readonly TraceLogWriter < BaseJsonApiOperationsController > _traceWriter ;
2629
2730 protected BaseJsonApiOperationsController ( IJsonApiOptions options , IResourceGraph resourceGraph , ILoggerFactory loggerFactory ,
28- IOperationsProcessor processor , IJsonApiRequest request , ITargetedFields targetedFields )
31+ IOperationsProcessor processor , IJsonApiRequest request , ITargetedFields targetedFields , IAtomicOperationFilter operationFilter )
2932 {
3033 ArgumentGuard . NotNull ( options ) ;
3134 ArgumentGuard . NotNull ( resourceGraph ) ;
3235 ArgumentGuard . NotNull ( loggerFactory ) ;
3336 ArgumentGuard . NotNull ( processor ) ;
3437 ArgumentGuard . NotNull ( request ) ;
3538 ArgumentGuard . NotNull ( targetedFields ) ;
39+ ArgumentGuard . NotNull ( operationFilter ) ;
3640
3741 _options = options ;
3842 _resourceGraph = resourceGraph ;
3943 _processor = processor ;
4044 _request = request ;
4145 _targetedFields = targetedFields ;
46+ _operationFilter = operationFilter ;
4247 _traceWriter = new TraceLogWriter < BaseJsonApiOperationsController > ( loggerFactory ) ;
4348 }
4449
@@ -111,6 +116,8 @@ public virtual async Task<IActionResult> PostOperationsAsync([FromBody] IList<Op
111116
112117 ArgumentGuard . NotNull ( operations ) ;
113118
119+ ValidateEnabledOperations ( operations ) ;
120+
114121 if ( _options . ValidateModelState )
115122 {
116123 ValidateModelState ( operations ) ;
@@ -120,6 +127,68 @@ public virtual async Task<IActionResult> PostOperationsAsync([FromBody] IList<Op
120127 return results . Any ( result => result != null ) ? Ok ( results ) : NoContent ( ) ;
121128 }
122129
130+ protected virtual void ValidateEnabledOperations ( IList < OperationContainer > operations )
131+ {
132+ List < ErrorObject > errors = [ ] ;
133+
134+ for ( int operationIndex = 0 ; operationIndex < operations . Count ; operationIndex ++ )
135+ {
136+ IJsonApiRequest operationRequest = operations [ operationIndex ] . Request ;
137+ WriteOperationKind operationKind = operationRequest . WriteOperation ! . Value ;
138+
139+ if ( operationRequest . Relationship != null && ! _operationFilter . IsEnabled ( operationRequest . Relationship . LeftType , operationKind ) )
140+ {
141+ string operationCode = GetOperationCodeText ( operationKind ) ;
142+
143+ errors . Add ( new ErrorObject ( HttpStatusCode . UnprocessableEntity )
144+ {
145+ Title = "The requested operation is not accessible." ,
146+ Detail = $ "The '{ operationCode } ' relationship operation is not accessible for relationship '{ operationRequest . Relationship } ' " +
147+ $ "on resource type '{ operationRequest . Relationship . LeftType } '.",
148+ Source = new ErrorSource
149+ {
150+ Pointer = $ "/atomic:operations[{ operationIndex } ]"
151+ }
152+ } ) ;
153+ }
154+ else if ( operationRequest . PrimaryResourceType != null && ! _operationFilter . IsEnabled ( operationRequest . PrimaryResourceType , operationKind ) )
155+ {
156+ string operationCode = GetOperationCodeText ( operationKind ) ;
157+
158+ errors . Add ( new ErrorObject ( HttpStatusCode . UnprocessableEntity )
159+ {
160+ Title = "The requested operation is not accessible." ,
161+ Detail = $ "The '{ operationCode } ' resource operation is not accessible for resource type '{ operationRequest . PrimaryResourceType } '.",
162+ Source = new ErrorSource
163+ {
164+ Pointer = $ "/atomic:operations[{ operationIndex } ]"
165+ }
166+ } ) ;
167+ }
168+ }
169+
170+ if ( errors . Count > 0 )
171+ {
172+ throw new JsonApiException ( errors ) ;
173+ }
174+ }
175+
176+ private static string GetOperationCodeText ( WriteOperationKind operationKind )
177+ {
178+ AtomicOperationCode operationCode = operationKind switch
179+ {
180+ WriteOperationKind . CreateResource => AtomicOperationCode . Add ,
181+ WriteOperationKind . UpdateResource => AtomicOperationCode . Update ,
182+ WriteOperationKind . DeleteResource => AtomicOperationCode . Remove ,
183+ WriteOperationKind . AddToRelationship => AtomicOperationCode . Add ,
184+ WriteOperationKind . SetRelationship => AtomicOperationCode . Update ,
185+ WriteOperationKind . RemoveFromRelationship => AtomicOperationCode . Remove ,
186+ _ => throw new NotSupportedException ( $ "Unknown operation kind '{ operationKind } '.")
187+ } ;
188+
189+ return operationCode . ToString ( ) . ToLowerInvariant ( ) ;
190+ }
191+
123192 protected virtual void ValidateModelState ( IList < OperationContainer > operations )
124193 {
125194 // We must validate the resource inside each operation manually, because they are typed as IIdentifiable.
0 commit comments