Skip to content

Commit 4f0e483

Browse files
authored
#683 Validate placeholder duplicates in path templates (#1927)
* add validation for downstream path also * add similiar route placeholders * Add unit tests * Refactor unit tests * IDE1006: Naming rule violation * IDE0028: Collection Initialization can be simplified * Merge into one theory * Less `IEnumerable<T>` usage to have less `IEnumerator<T>` objects in favor of the collection one * Refactor validation of duplicated placeholders * Finish unit testing * Publish hidden Service Fabric feature * Update acceptance tests * Update integration tests * Update release notes --------- The author: Aly Kafoury <@AlyHKafoury> Co-authored-by: Raman Maksimchuk <[email protected]>
1 parent ded4d7e commit 4f0e483

File tree

9 files changed

+374
-198
lines changed

9 files changed

+374
-198
lines changed

Diff for: ReleaseNotes.md

+9-28
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,20 @@
1-
## January 2024 (version {0}) aka [Hornussen](https://www.myswitzerland.com/en-ch/planning/about-switzerland/custom-and-tradition/hornussen-where-the-nouss-flies-from-the-ramp-and-into-the-playing-field/) release
2-
> Codenamed as **[Hornussen Sport](https://www.youtube.com/results?search_query=Hornussen)**
3-
> Read the Docs: [Ocelot 23.1](https://ocelot.readthedocs.io/en/23.1.0/)
1+
## February 2024 (version {0}) aka [February'24](https://github.com/ThreeMammals/Ocelot/milestone/5) release
2+
> Codenamed as **[February'24](https://github.com/ThreeMammals/Ocelot/milestone/5)**
3+
> Read the Docs: [Ocelot 23.2](https://ocelot.readthedocs.io/en/23.2.0/)
44
55
### Focus On
66

77
<details>
8-
<summary><b>Multiplexing middleware</b> aka <a href="https://ocelot.readthedocs.io/en/latest/features/requestaggregation.html">Request Aggregation</a> feature</summary>
8+
<summary><b>New features of</b>: Service Fabric and ...</summary>
99

10-
- Significant refactoring and design review of the [Multiplexer](https://github.com/ThreeMammals/Ocelot/tree/develop/src/Ocelot/Multiplexer)
11-
- Optimizing multiplexer performance: `HttpContext` is not copied when there is only one downstream route, and etc.
12-
- Fixed [the bug](https://github.com/ThreeMammals/Ocelot/pull/1462) in the multiplexer: `HttpContext.User` information was not copied if there was more than one downstream request.
13-
</details>
14-
15-
<details>
16-
<summary><b>System routing</b>. Content streaming when <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding">Transfer-Encoding</a>: 'chunked'</summary>
17-
18-
- Correction of [the bug](https://github.com/ThreeMammals/Ocelot/pull/1972) when creating requests: The header [Transfer-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding): `chunked` was present even when there was no content or the request body size was 0. These cases are now addressed.
19-
</details>
20-
21-
<details>
22-
<summary><b>Updates of the features</b>: QoS, Load Balancer and Error Status Codes</summary>
23-
24-
- [Quality of Service](https://ocelot.readthedocs.io/en/latest/features/qualityofservice.html): Possibility of implementation of custom Polly v8.2 providers. New `AddPolly` extension methods.
25-
- [Load Balancer](https://ocelot.readthedocs.io/en/latest/features/loadbalancer.html): Extension of the route key format, ensuring that the key remains unique for cases of **UpstreamHost** route property and **ServiceName** vs **ServiceNamespace** properties in Consul setup.
26-
- [Error Status Codes](https://ocelot.readthedocs.io/en/latest/features/errorcodes.html): When [413 Content Too Large](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413), Ocelot now returns a 413 `PayloadTooLargeError` (Ocelot error code `41`).
27-
</details>
28-
29-
<details>
30-
<summary>Documentation for <b>Request Aggregation</b></summary>
31-
32-
- [Request Aggregation](https://ocelot.readthedocs.io/en/latest/features/requestaggregation.html)
10+
- **[Service Fabric](https://ocelot.readthedocs.io/en/latest/features/servicefabric.html)**: Published old undocumented "[Placeholders in Service Name](https://ocelot.readthedocs.io/en/23.2.0/features/servicefabric.html#placeholders-in-service-name)" feature of [Service Fabric](https://ocelot.readthedocs.io/en/23.2.0/features/servicefabric.html) [service discovery provider](https://ocelot.readthedocs.io/en/23.2.0/search.html?q=ServiceDiscoveryProvider). This feature is available starting from version [13.0.0](https://github.com/ThreeMammals/Ocelot/releases/tag/13.0.0).
3311
</details>
3412

3513
<details>
3614
<summary><b>Stabilization</b> aka bug fixing</summary>
3715

38-
- See [all bugs](https://github.com/ThreeMammals/Ocelot/issues?q=is%3Aissue+is%3Aclosed+label%3Abug+milestone%3AJanuary%2724) of the [January'24](https://github.com/ThreeMammals/Ocelot/milestone/4) milestone
16+
- [683](https://github.com/ThreeMammals/Ocelot/issues/683) by PR [1927](https://github.com/ThreeMammals/Ocelot/pull/1927)
17+
Ocelot configuration validation logic has updated with [new rules](https://github.com/search?q=repo%3AThreeMammals%2FOcelot+IsPlaceholderNotDuplicatedIn+IsUpstreamPlaceholderDefinedInDownstream+IsDownstreamPlaceholderDefinedInUpstream&type=code) to search for placeholder duplicates in path templates.
18+
See more in the [FileConfigurationFluentValidator](https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20FileConfigurationFluentValidator&type=code) class.
19+
- See [all bugs](https://github.com/ThreeMammals/Ocelot/issues?q=is%3Aissue+is%3Aclosed+label%3Abug+milestone%3AFebruary%2724) of the [February'24](https://github.com/ThreeMammals/Ocelot/milestone/5) milestone
3920
</details>

Diff for: docs/features/servicefabric.rst

+58-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ We also need to set up the **ServiceDiscoveryProvider** in **GlobalConfiguration
99
The example here shows a typical configuration.
1010
It assumes *Service Fabric* is running on ``localhost`` and that the naming service is on port ``19081``.
1111

12-
The example below is taken from the `samples/OcelotServiceFabric <https://github.com/ThreeMammals/Ocelot/tree/main/samples/OcelotServiceFabric>`_ folder so please check it if this doesn't make sense!
12+
The example below is taken from the `OcelotServiceFabric <https://github.com/ThreeMammals/Ocelot/tree/main/samples/OcelotServiceFabric>`_ sample, so please check it if this doesn't make sense!
1313

1414
.. code-block:: json
1515
@@ -24,6 +24,7 @@ The example below is taken from the `samples/OcelotServiceFabric <https://github
2424
}
2525
],
2626
"GlobalConfiguration": {
27+
"BaseUrl": "https://ocelot.com"
2728
"RequestIdKey": "OcRequestId",
2829
"ServiceDiscoveryProvider": {
2930
"Host": "localhost",
@@ -36,6 +37,61 @@ The example below is taken from the `samples/OcelotServiceFabric <https://github
3637
If you are using stateless / guest exe services, Ocelot will be able to proxy through the naming service without anything else.
3738
However, if you are using statefull / actor services, you must send the **PartitionKind** and **PartitionKey** query string values with the client request e.g.
3839

39-
GET http://ocelot.com/EquipmentInterfaces?PartitionKind=xxx&PartitionKey=xxx
40+
GET ``http://ocelot.com/EquipmentInterfaces?PartitionKind=xxx&PartitionKey=xxx``
4041

4142
There is no way for Ocelot to work these out for you.
43+
44+
.. _sf-placeholders:
45+
46+
Placeholders in Service Name [#f1]_
47+
-----------------------------------
48+
49+
In Ocelot, you can insert placeholders for variables into your ``UpstreamPathTemplate`` and ``ServiceName`` using the format ``{something}``.
50+
51+
It's important to note that the placeholder variable must exist in both the (**DownstreamPathTemplate** vs **ServiceName**) and the **UpstreamPathTemplate**.
52+
The **UpstreamPathTemplate** should include all placeholders from the **DownstreamPathTemplate** and **ServiceName**;
53+
otherwise, Ocelot will not start due to validation errors, which are logged.
54+
55+
Once the validation stage is cleared, Ocelot will replace the placeholder values in the **UpstreamPathTemplate** with those in the **DownstreamPathTemplate** and/or **ServiceName** for each processed request.
56+
Thus, the :ref:`sf-placeholders` behave similarly to the :ref:`routing-placeholders` feature, but with the **ServiceName** property considered during the processing.
57+
58+
59+
Placeholders example
60+
^^^^^^^^^^^^^^^^^^^^
61+
62+
Here is the example of variable ``version`` in *Service Fabric* service name.
63+
64+
**Given** you have the following `ocelot.json`_:
65+
66+
.. code-block:: json
67+
68+
{
69+
"Routes": [
70+
{
71+
"UpstreamPathTemplate": "/api/{version}/{endpoint}",
72+
"DownstreamPathTemplate": "/{endpoint}",
73+
"ServiceName": "Service_{version}/Api",
74+
}
75+
],
76+
"GlobalConfiguration": {
77+
"BaseUrl": "https://ocelot.com"
78+
"ServiceDiscoveryProvider": {
79+
"Host": "localhost",
80+
"Port": 19081,
81+
"Type": "ServiceFabric"
82+
}
83+
}
84+
}
85+
86+
**When** you make a request: GET ``https://ocelot.com/api/1.0/products``
87+
88+
**Then** the *Service Fabric* request: GET ``http://localhost:19081/Service_1.0/Api/products``
89+
90+
""""
91+
92+
.. [#f1] ":ref:`sf-placeholders`" feature was requested in issue `721`_ and delivered by PR `722`_ as a part of the version `13.0.0`_.
93+
94+
.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json
95+
.. _721: https://github.com/ThreeMammals/Ocelot/issues/721
96+
.. _722: https://github.com/ThreeMammals/Ocelot/pull/722
97+
.. _13.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/13.0.0

Diff for: src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs

+63-12
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77

88
namespace Ocelot.Configuration.Validator
99
{
10-
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
10+
/// <summary>
11+
/// Validation of a <see cref="FileConfiguration"/> objects.
12+
/// </summary>
13+
public partial class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
1114
{
1215
private const string Servicefabric = "servicefabric";
13-
private readonly List<ServiceDiscoveryFinderDelegate> _serviceDiscoveryFinderDelegates;
16+
private readonly List<ServiceDiscoveryFinderDelegate> _serviceDiscoveryFinderDelegates;
1417

1518
public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentValidator routeFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator)
1619
{
@@ -34,7 +37,17 @@ public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentVa
3437

3538
RuleForEach(configuration => configuration.Routes)
3639
.Must((_, route) => IsPlaceholderNotDuplicatedIn(route.UpstreamPathTemplate))
37-
.WithMessage((_, route) => $"{nameof(route)} {route.UpstreamPathTemplate} has duplicated placeholder");
40+
.WithMessage((_, route) => $"{nameof(route.UpstreamPathTemplate)} '{route.UpstreamPathTemplate}' has duplicated placeholder");
41+
RuleForEach(configuration => configuration.Routes)
42+
.Must((_, route) => IsPlaceholderNotDuplicatedIn(route.DownstreamPathTemplate))
43+
.WithMessage((_, route) => $"{nameof(route.DownstreamPathTemplate)} '{route.DownstreamPathTemplate}' has duplicated placeholder");
44+
45+
RuleForEach(configuration => configuration.Routes)
46+
.Must(IsUpstreamPlaceholderDefinedInDownstream)
47+
.WithMessage((_, route) => $"{nameof(route.UpstreamPathTemplate)} '{route.UpstreamPathTemplate}' doesn't contain the same placeholders in {nameof(route.DownstreamPathTemplate)} '{route.DownstreamPathTemplate}'");
48+
RuleForEach(configuration => configuration.Routes)
49+
.Must(IsDownstreamPlaceholderDefinedInUpstream)
50+
.WithMessage((_, route) => $"{nameof(route.DownstreamPathTemplate)} '{route.DownstreamPathTemplate}' doesn't contain the same placeholders in {nameof(route.UpstreamPathTemplate)} '{route.UpstreamPathTemplate}'");
3851

3952
RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider)
4053
.Must(HaveServiceDiscoveryProviderRegistered)
@@ -93,14 +106,52 @@ private static bool AllRoutesForAggregateExist(FileAggregateRoute fileAggregateR
93106

94107
return routesForAggregate.Count() == fileAggregateRoute.RouteKeys.Count;
95108
}
96-
97-
private static bool IsPlaceholderNotDuplicatedIn(string upstreamPathTemplate)
109+
110+
#if NET7_0_OR_GREATER
111+
[GeneratedRegex(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
112+
private static partial Regex PlaceholderRegex();
113+
#else
114+
private static readonly Regex PlaceholderRegexVar = new(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
115+
private static Regex PlaceholderRegex() => PlaceholderRegexVar;
116+
#endif
117+
118+
private static bool IsPlaceholderNotDuplicatedIn(string pathTemplate)
98119
{
99-
var regExPlaceholder = new Regex("{[^}]+}");
100-
var matches = regExPlaceholder.Matches(upstreamPathTemplate);
101-
var upstreamPathPlaceholders = matches.Select(m => m.Value);
102-
return upstreamPathPlaceholders.Count() == upstreamPathPlaceholders.Distinct().Count();
103-
}
120+
var placeholders = PlaceholderRegex().Matches(pathTemplate)
121+
.Select(m => m.Value).ToList();
122+
return placeholders.Count == placeholders.Distinct().Count();
123+
}
124+
125+
private static bool IsServiceFabricWithServiceName(FileConfiguration configuration, FileRoute route)
126+
=> Servicefabric.Equals(configuration?.GlobalConfiguration?.ServiceDiscoveryProvider?.Type, StringComparison.InvariantCultureIgnoreCase)
127+
&& !string.IsNullOrEmpty(route?.ServiceName) && PlaceholderRegex().IsMatch(route.ServiceName);
128+
129+
private bool IsUpstreamPlaceholderDefinedInDownstream(FileConfiguration configuration, FileRoute route)
130+
=> IsServiceFabricWithServiceName(configuration, route)
131+
? IsPlaceholderDefinedInBothTemplates(route.UpstreamPathTemplate, route.ServiceName + route.DownstreamPathTemplate)
132+
: IsPlaceholderDefinedInBothTemplates(route.UpstreamPathTemplate, route.DownstreamPathTemplate);
133+
134+
private bool IsDownstreamPlaceholderDefinedInUpstream(FileConfiguration configuration, FileRoute route)
135+
=> IsServiceFabricWithServiceName(configuration, route)
136+
? IsPlaceholderDefinedInBothTemplates(route.ServiceName + route.DownstreamPathTemplate, route.UpstreamPathTemplate)
137+
: IsPlaceholderDefinedInBothTemplates(route.DownstreamPathTemplate, route.UpstreamPathTemplate);
138+
139+
private static bool IsPlaceholderDefinedInBothTemplates(string firstPathTemplate, string secondPathTemplate)
140+
{
141+
var firstPlaceholders = PlaceholderRegex().Matches(firstPathTemplate)
142+
.Select(m => m.Value).ToList();
143+
var secondPlaceholders = PlaceholderRegex().Matches(secondPathTemplate)
144+
.Select(m => m.Value).ToList();
145+
foreach (var placeholder in firstPlaceholders)
146+
{
147+
if (!secondPlaceholders.Contains(placeholder))
148+
{
149+
return false;
150+
}
151+
}
152+
153+
return true;
154+
}
104155

105156
private static bool DoesNotContainRoutesWithSpecificRequestIdKeys(FileAggregateRoute fileAggregateRoute,
106157
IEnumerable<FileRoute> routes)
@@ -131,7 +182,7 @@ private static bool IsNotDuplicateIn(FileRoute route,
131182

132183
var duplicateSpecificVerbs = matchingRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any();
133184

134-
if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs))
185+
if (duplicateAllowAllVerbs || duplicateSpecificVerbs || allowAllVerbs && specificVerbs)
135186
{
136187
return false;
137188
}
@@ -155,6 +206,6 @@ private static bool IsNotDuplicateIn(FileAggregateRoute route, IEnumerable<FileA
155206
var matchingRoutes = aggregateRoutes
156207
.Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate & r.UpstreamHost == route.UpstreamHost);
157208
return matchingRoutes.Count() <= 1;
158-
}
209+
}
159210
}
160211
}

Diff for: test/Ocelot.AcceptanceTests/AggregateTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public void Should_return_response_200_with_advanced_aggregate_configs()
187187
Port = port2,
188188
},
189189
],
190-
UpstreamPathTemplate = "/UserDetails",
190+
UpstreamPathTemplate = "/UserDetails/{userId}",
191191
UpstreamHttpMethod = ["Get"],
192192
Key = "UserDetails",
193193
},
@@ -203,7 +203,7 @@ public void Should_return_response_200_with_advanced_aggregate_configs()
203203
Port = port3,
204204
},
205205
],
206-
UpstreamPathTemplate = "/PostDetails",
206+
UpstreamPathTemplate = "/PostDetails/{postId}",
207207
UpstreamHttpMethod = ["Get"],
208208
Key = "PostDetails",
209209
},

Diff for: test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void should_return_200_and_change_downstream_path()
6161
},
6262
},
6363
DownstreamScheme = "http",
64-
UpstreamPathTemplate = "/users",
64+
UpstreamPathTemplate = "/users/{userId}",
6565
UpstreamHttpMethod = new List<string> { "Get" },
6666
AuthenticationOptions = new FileAuthenticationOptions
6767
{

0 commit comments

Comments
 (0)