Skip to content

Commit 5150fad

Browse files
authored
Prohibit specifying primary key route for REST POST requests (#1364)
## Why make this change? Fixes #1365, to prohibit specifying primary key route in REST POST requests. ## What is this change? Added an extra validation to check if primary key route is non-empty for POST requests. ## Additional changes Validations for primary key route and query string were of similar nature for all the operations but had separate functions created for each of the operations. Removed those specific functions and created a new function `RequestValidator.ValidatePrimaryKeyRouteAndQueryStringInURL` which does those validations for all of these operations. ## How was this tested? - [x] Integration Tests ## Sample Request(s) - Example REST POST request: URL: https://localhost:5001/api/commodities/categoryid/0/pieceid/6 Body: ``` { "categoryid": 0, "pieceid": 6, "categoryName": "SciFi" } ``` Response: ``` { "error": { "code": "BadRequest", "message": "Primary key for POST request can't be specified in request URL. Use request body instead.", "status": 400 } } ``` In current scenario, we would have allowed such a request to execute. Also, even if the url parameters here were categoryid/0/pieceid/7, the record still would have been created corresponding to categoryid=6 and pieceid=0 as specified in the request body, thus obviously causing confusion to the user.
1 parent 7953d06 commit 5150fad

File tree

4 files changed

+86
-56
lines changed

4 files changed

+86
-56
lines changed

src/Service.Tests/SqlTests/RestApiTests/Insert/InsertApiTestBase.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,13 @@ await SetupAndRunRestApiTest(
263263

264264
#region Negative Tests
265265

266+
/// <summary>
267+
/// Test to validate that an exception is encountered when the user specifies primary key route or query string for a POST request via REST.
268+
/// </summary>
266269
[TestMethod]
267-
public virtual async Task InsertOneWithInvalidQueryStringTest()
270+
public virtual async Task InsertOneWithPrimaryKeyOrQueryStringInURLTest()
268271
{
272+
// Validate that a POST request is not allowed to include a query string in the URL.
269273
string requestBody = @"
270274
{
271275
""title"": ""My New Book"",
@@ -274,7 +278,7 @@ public virtual async Task InsertOneWithInvalidQueryStringTest()
274278

275279
await SetupAndRunRestApiTest(
276280
primaryKeyRoute: string.Empty,
277-
queryString: "?/id/5001",
281+
queryString: "?$filter=id eq 5001",
278282
entityNameOrPath: _integrationEntityName,
279283
sqlQuery: string.Empty,
280284
operationType: Config.Operation.Insert,
@@ -283,6 +287,26 @@ await SetupAndRunRestApiTest(
283287
expectedErrorMessage: RequestValidator.QUERY_STRING_INVALID_USAGE_ERR_MESSAGE,
284288
expectedStatusCode: HttpStatusCode.BadRequest
285289
);
290+
291+
//Validate that a POST request is not allowed to include primary key in the URL.
292+
requestBody = @"
293+
{
294+
""categoryid"": 0,
295+
""pieceid"": 4,
296+
""categoryName"": ""SciFi""
297+
}";
298+
299+
await SetupAndRunRestApiTest(
300+
primaryKeyRoute: "categoryid/0/pieceid/4",
301+
queryString: string.Empty,
302+
entityNameOrPath: _integrationEntityName,
303+
sqlQuery: string.Empty,
304+
operationType: Config.Operation.Insert,
305+
requestBody: requestBody,
306+
exceptionExpected: true,
307+
expectedErrorMessage: RequestValidator.PRIMARY_KEY_INVALID_USAGE_ERR_MESSAGE,
308+
expectedStatusCode: HttpStatusCode.BadRequest
309+
);
286310
}
287311

288312
/// <summary>

src/Service.Tests/Unittests/RequestValidatorUnitTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,15 @@ public void RequestBodyJsonParsing(string requestBody, bool expectsException)
160160
if (expectsException)
161161
{
162162
DataApiBuilderException dabException = Assert.ThrowsException<DataApiBuilderException>(
163-
action: () => RequestValidator.ParseRequestBody(requestBody),
163+
action: () => RequestValidator.ValidateAndParseRequestBody(requestBody),
164164
message: RequestValidator.REQUEST_BODY_INVALID_JSON_ERR_MESSAGE);
165165

166166
Assert.AreEqual(expected: HttpStatusCode.BadRequest, actual: dabException.StatusCode);
167167
Assert.AreEqual(expected: DataApiBuilderException.SubStatusCodes.BadRequest, actual: dabException.SubStatusCode);
168168
}
169169
else
170170
{
171-
RequestValidator.ParseRequestBody(requestBody);
171+
RequestValidator.ValidateAndParseRequestBody(requestBody);
172172
}
173173
}
174174
#endregion

src/Service/Services/RequestValidator.cs

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class RequestValidator
2020
public const string REQUEST_BODY_INVALID_JSON_ERR_MESSAGE = "Request body contains invalid JSON.";
2121
public const string BATCH_MUTATION_UNSUPPORTED_ERR_MESSAGE = "A Mutation operation on more than one entity in a single request is not yet supported.";
2222
public const string QUERY_STRING_INVALID_USAGE_ERR_MESSAGE = "Query string for this HTTP request type is an invalid URL.";
23+
public const string PRIMARY_KEY_INVALID_USAGE_ERR_MESSAGE = "Primary key for POST requests can't be specified in the request URL. Use request body instead.";
2324
public const string PRIMARY_KEY_NOT_PROVIDED_ERR_MESSAGE = "Primary Key for this HTTP request type is required.";
2425

2526
/// <summary>
@@ -177,13 +178,14 @@ public static void ValidateStoredProcedureRequestContext(
177178
}
178179

179180
/// <summary>
180-
/// Validates the primarykeyroute is populated with respect to an Update or Upsert operation.
181+
/// Validates the request body is a valid JSON string and is not a JSON array since we only
182+
/// support point operations.
181183
/// </summary>
182184
/// <param name="requestBody">Request body content</param>
183185
/// <exception cref="DataApiBuilderException">Thrown when request body is invalid JSON
184186
/// or JSON is a batch request (mutation of more than one entity in a single request).</exception>
185187
/// <returns>JsonElement representing the body of the request.</returns>
186-
public static JsonElement ParseRequestBody(string requestBody)
188+
public static JsonElement ValidateAndParseRequestBody(string requestBody)
187189
{
188190
JsonElement mutationPayloadRoot = new();
189191

@@ -217,56 +219,58 @@ public static JsonElement ParseRequestBody(string requestBody)
217219
}
218220

219221
/// <summary>
220-
/// Validates the request body and queryString with respect to an Insert operation.
222+
/// Performs validations on primary key route and queryString specified in the request URL, whose specifics depend on the type of operation being executed.
223+
/// Eg. a POST request cannot have primary key route/query string in the URL while it is mandatory for a PUT/PATCH/DELETE request to have a primary key route in the URL.
221224
/// </summary>
225+
/// <param name="operationType">Type of operation being executed.</param>
226+
/// <param name="primaryKeyRoute">URL route e.g. "Entity/id/1"</param>
222227
/// <param name="queryString">queryString e.g. "$?filter="</param>
223-
/// <param name="requestBody">The string JSON body from the request.</param>
224-
/// <exception cref="DataApiBuilderException">Raised when queryString is present or invalid JSON in requestBody is found.</exception>
225-
public static JsonElement ValidateInsertRequest(string queryString, string requestBody)
228+
/// <exception cref="DataApiBuilderException">Raised when primaryKeyRoute/queryString fail the validations for the operation.</exception>
229+
public static void ValidatePrimaryKeyRouteAndQueryStringInURL(Config.Operation operationType, string? primaryKeyRoute = null, string? queryString = null)
226230
{
227-
if (!string.IsNullOrEmpty(queryString))
228-
{
229-
throw new DataApiBuilderException(
230-
message: QUERY_STRING_INVALID_USAGE_ERR_MESSAGE,
231-
statusCode: HttpStatusCode.BadRequest,
232-
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
233-
}
231+
bool isPrimaryKeyRouteEmpty = string.IsNullOrEmpty(primaryKeyRoute);
232+
bool isQueryStringEmpty = string.IsNullOrEmpty(queryString);
234233

235-
return ParseRequestBody(requestBody);
236-
}
237-
238-
/// <summary>
239-
/// Validates the primarykeyroute is populated with respect to an Update or Upsert operation.
240-
/// </summary>
241-
/// <param name="primaryKeyRoute">URL route e.g. "Entity/id/1"</param>
242-
/// <param name="requestBody">The string JSON body from the request.</param>
243-
/// <exception cref="DataApiBuilderException">Raised when either primary key route is absent or invalid JSON in requestBody is found.</exception>
244-
public static JsonElement ValidateUpdateOrUpsertRequest(string? primaryKeyRoute, string requestBody)
245-
{
246-
if (string.IsNullOrWhiteSpace(primaryKeyRoute))
234+
switch (operationType)
247235
{
248-
throw new DataApiBuilderException(
249-
message: PRIMARY_KEY_NOT_PROVIDED_ERR_MESSAGE,
250-
statusCode: HttpStatusCode.BadRequest,
251-
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
252-
}
236+
case Config.Operation.Insert:
237+
if (!isPrimaryKeyRouteEmpty)
238+
{
239+
throw new DataApiBuilderException(
240+
message: PRIMARY_KEY_INVALID_USAGE_ERR_MESSAGE,
241+
statusCode: HttpStatusCode.BadRequest,
242+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
243+
}
253244

254-
return ParseRequestBody(requestBody);
255-
}
245+
if (!isQueryStringEmpty)
246+
{
247+
throw new DataApiBuilderException(
248+
message: QUERY_STRING_INVALID_USAGE_ERR_MESSAGE,
249+
statusCode: HttpStatusCode.BadRequest,
250+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
251+
}
256252

257-
/// <summary>
258-
/// Validates the primarykeyroute is populated with respect to a Delete operation.
259-
/// </summary>
260-
/// <param name="primaryKeyRoute">URL route e.g. "Entity/id/1"</param>
261-
/// <exception cref="DataApiBuilderException">Raised when primary key route is absent</exception>
262-
public static void ValidateDeleteRequest(string? primaryKeyRoute)
263-
{
264-
if (string.IsNullOrWhiteSpace(primaryKeyRoute))
265-
{
266-
throw new DataApiBuilderException(
267-
message: PRIMARY_KEY_NOT_PROVIDED_ERR_MESSAGE,
268-
statusCode: HttpStatusCode.BadRequest,
269-
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
253+
break;
254+
case Config.Operation.Delete:
255+
case Config.Operation.Update:
256+
case Config.Operation.UpdateIncremental:
257+
case Config.Operation.Upsert:
258+
case Config.Operation.UpsertIncremental:
259+
/// Validate that the primarykeyroute is populated for these operations.
260+
if (isPrimaryKeyRouteEmpty)
261+
{
262+
throw new DataApiBuilderException(
263+
message: PRIMARY_KEY_NOT_PROVIDED_ERR_MESSAGE,
264+
statusCode: HttpStatusCode.BadRequest,
265+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
266+
}
267+
268+
break;
269+
default:
270+
throw new DataApiBuilderException(
271+
message: "Unexpected operation encountered. Cannot perform validations on URL components.",
272+
statusCode: HttpStatusCode.InternalServerError,
273+
subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
270274
}
271275
}
272276

src/Service/Services/RestService.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ RuntimeConfigProvider runtimeConfigProvider
117117
isList: string.IsNullOrEmpty(primaryKeyRoute));
118118
break;
119119
case Config.Operation.Insert:
120-
JsonElement insertPayloadRoot = RequestValidator.ValidateInsertRequest(queryString, requestBody);
120+
RequestValidator.ValidatePrimaryKeyRouteAndQueryStringInURL(operationType, primaryKeyRoute, queryString);
121+
JsonElement insertPayloadRoot = RequestValidator.ValidateAndParseRequestBody(requestBody);
121122
context = new InsertRequestContext(
122123
entityName,
123124
dbo: dbObject,
@@ -132,7 +133,7 @@ RuntimeConfigProvider runtimeConfigProvider
132133

133134
break;
134135
case Config.Operation.Delete:
135-
RequestValidator.ValidateDeleteRequest(primaryKeyRoute);
136+
RequestValidator.ValidatePrimaryKeyRouteAndQueryStringInURL(operationType, primaryKeyRoute);
136137
context = new DeleteRequestContext(entityName,
137138
dbo: dbObject,
138139
isList: false);
@@ -141,7 +142,8 @@ RuntimeConfigProvider runtimeConfigProvider
141142
case Config.Operation.UpdateIncremental:
142143
case Config.Operation.Upsert:
143144
case Config.Operation.UpsertIncremental:
144-
JsonElement upsertPayloadRoot = RequestValidator.ValidateUpdateOrUpsertRequest(primaryKeyRoute, requestBody);
145+
RequestValidator.ValidatePrimaryKeyRouteAndQueryStringInURL(operationType, primaryKeyRoute);
146+
JsonElement upsertPayloadRoot = RequestValidator.ValidateAndParseRequestBody(requestBody);
145147
context = new UpsertRequestContext(
146148
entityName,
147149
dbo: dbObject,
@@ -270,10 +272,10 @@ private void PopulateStoredProcedureContext(
270272
case Config.Operation.UpdateIncremental:
271273
case Config.Operation.Upsert:
272274
case Config.Operation.UpsertIncremental:
273-
// Stored procedure call is semantically identical for all methods except Find, so we can
274-
// effectively reuse the ValidateInsertRequest - throws error if query string is nonempty
275-
// and parses the body into json
276-
JsonElement requestPayloadRoot = RequestValidator.ValidateInsertRequest(queryString, requestBody);
275+
// Stored procedure call is semantically identical for all methods except Find.
276+
// So, we can effectively treat it as Insert operation - throws error if query string is non empty.
277+
RequestValidator.ValidatePrimaryKeyRouteAndQueryStringInURL(Config.Operation.Insert, queryString);
278+
JsonElement requestPayloadRoot = RequestValidator.ValidateAndParseRequestBody(requestBody);
277279
context = new StoredProcedureRequestContext(
278280
entityName,
279281
dbo: dbObject,

0 commit comments

Comments
 (0)