diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index f1155fb74d..f6758a7882 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -418,6 +418,7 @@ public void TestUpdateEnabledForRestSettings(bool updatedEnabledValue) [DataRow("/updatedPath", DisplayName = "Update REST path to /updatedPath.")] [DataRow("/updated_Path", DisplayName = "Ensure underscore is allowed in REST path.")] [DataRow("/updated-Path", DisplayName = "Ensure hyphen is allowed in REST path.")] + [DataRow("/api/v2", DisplayName = "Ensure multi-segment paths are allowed in REST path.")] public void TestUpdatePathForRestSettings(string updatedPathValue) { // Arrange -> all the setup which includes creating options. diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index f625dba101..ef044f1dc5 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -343,11 +343,12 @@ public void TestUpdateDepthLimitInGraphQLRuntimeSettings(string depthLimit, bool [DataRow("/updatedPath", true, DisplayName = "Success in updated GraphQL Path to /updatedPath.")] [DataRow("/updated-Path", true, DisplayName = "Success in updated GraphQL Path to /updated-Path.")] [DataRow("/updated_Path", true, DisplayName = "Success in updated GraphQL Path to /updated_Path.")] + [DataRow("/api/v2", true, DisplayName = "Success in updated GraphQL Path to multi-segment path /api/v2.")] [DataRow("updatedPath", false, DisplayName = "Failure due to '/' missing.")] [DataRow("/updated Path", false, DisplayName = "Failure due to white spaces.")] [DataRow("/updated.Path", false, DisplayName = "Failure due to reserved char '.'.")] [DataRow("/updated@Path", false, DisplayName = "Failure due reserved chars '@'.")] - [DataRow("/updated/Path", false, DisplayName = "Failure due reserved chars '/'.")] + [DataRow("/api//v2", false, DisplayName = "Failure due to empty path segment.")] public void TestUpdateGraphQLPathRuntimeSettings(string path, bool isSuccess) { // Initialize the config file. @@ -405,11 +406,12 @@ public void TestUpdateHostCorsOriginsRuntimeSettings(string path, bool isSuccess [DataRow("/updatedPath", true, DisplayName = "Successfully updated Rest Path to /updatedPath.")] [DataRow("/updated-Path", true, DisplayName = "Successfully updated Rest Path to /updated-Path.")] [DataRow("/updated_Path", true, DisplayName = "Successfully updated Rest Path to /updated_Path.")] + [DataRow("/api/v2", true, DisplayName = "Successfully updated Rest Path to multi-segment path /api/v2.")] [DataRow("updatedPath", false, DisplayName = "Failure due to '/' missing.")] [DataRow("/updated Path", false, DisplayName = "Failure due to white spaces.")] [DataRow("/updated.Path", false, DisplayName = "Failure due to reserved char '.'.")] [DataRow("/updated@Path", false, DisplayName = "Failure due reserved chars '@'.")] - [DataRow("/updated/Path", false, DisplayName = "Failure due reserved chars '/'.")] + [DataRow("/api//v2", false, DisplayName = "Failure due to empty path segment.")] public void TestUpdateRestPathRuntimeSettings(string path, bool isSuccess) { // Initialize the config file. diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs index 9367f9260f..99f14c1b2a 100644 --- a/src/Cli/Utils.cs +++ b/src/Cli/Utils.cs @@ -205,7 +205,8 @@ public static bool IsURIComponentValid(string? uriComponent) uriComponent = uriComponent.Substring(1); } - return !RuntimeConfigValidatorUtil.DoesUriComponentContainReservedChars(uriComponent); + // The path may contain multiple '/'-separated segments (e.g. 'api/v2'); validate each segment. + return !RuntimeConfigValidatorUtil.DoesUriPathContainReservedChars(uriComponent); } /// diff --git a/src/Core/Configurations/RuntimeConfigValidatorUtil.cs b/src/Core/Configurations/RuntimeConfigValidatorUtil.cs index fce7821840..e80f7314bb 100644 --- a/src/Core/Configurations/RuntimeConfigValidatorUtil.cs +++ b/src/Core/Configurations/RuntimeConfigValidatorUtil.cs @@ -45,9 +45,10 @@ public static bool TryValidateUriComponent(string? uriComponent, out string exce } else { + // Remove the leading '/' before validating the remaining path. The path may contain + // multiple '/'-separated segments (e.g. '/api/v2'), each of which is validated individually. uriComponent = uriComponent.Substring(1); - // URI component should not contain any reserved characters. - if (DoesUriComponentContainReservedChars(uriComponent)) + if (DoesUriPathContainReservedChars(uriComponent)) { exceptionMessageSuffix = URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG; } @@ -66,6 +67,34 @@ public static bool DoesUriComponentContainReservedChars(string uriComponent) return _reservedUriCharsRgx.IsMatch(uriComponent); } + /// + /// Method to validate a URI path that may contain multiple '/'-separated segments + /// (for example 'api/v2'). The leading '/' is expected to already be removed. + /// Each segment is validated to ensure it is non-empty and free of reserved characters. + /// An empty input (representing the root path '/') is considered valid. + /// + /// Path prefix for rest/graphql apis with the leading '/' already removed. + /// true if any segment is empty or contains reserved characters, false otherwise. + public static bool DoesUriPathContainReservedChars(string uriPath) + { + // An empty path represents the root '/' which is valid and contains no segments to validate. + if (string.IsNullOrEmpty(uriPath)) + { + return false; + } + + foreach (string segment in uriPath.Split('/')) + { + // An empty segment indicates leading, consecutive, or trailing slashes. + if (string.IsNullOrEmpty(segment) || DoesUriComponentContainReservedChars(segment)) + { + return true; + } + } + + return false; + } + /// /// Method to validate an entity REST path allowing sub-directories (forward slashes). /// Each segment of the path is validated for reserved characters and path traversal patterns. diff --git a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs index bbb4874d1a..178115c86c 100644 --- a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs @@ -1683,6 +1683,14 @@ private static void ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(S DisplayName = "GraphQL path prefix containing space at the start and underscore in between.")] [DataRow("/", null, ApiType.REST, false, DisplayName = "REST path containing only a forward slash.")] + [DataRow("/api/v2", null, ApiType.REST, false, + DisplayName = "REST path containing multiple segments.")] + [DataRow("/api/v2", null, ApiType.GraphQL, false, + DisplayName = "GraphQL path containing multiple segments.")] + [DataRow("/api/", $"REST path {RuntimeConfigValidatorUtil.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG}", ApiType.REST, true, + DisplayName = "REST path containing a trailing slash.")] + [DataRow("/api//v2", $"REST path {RuntimeConfigValidatorUtil.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG}", ApiType.REST, true, + DisplayName = "REST path containing an empty segment.")] public void ValidateApiURIsAreWellFormed( string apiPathPrefix, string expectedErrorMessage,