Skip to content

Commit 3e2a3b7

Browse files
authored
Fixes APIM policies when using embedded C# with quotes Azure#3184 (Azure#3187)
1 parent 8d81817 commit 3e2a3b7

File tree

9 files changed

+137
-9
lines changed

9 files changed

+137
-9
lines changed

docs/CHANGELOG-v1.md

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
2929

3030
## Unreleased
3131

32+
What's changed since pre-release v1.40.0-B0147:
33+
34+
- Bug fixes:
35+
- Fixed evaluation of APIM policies when using embedded C# with quotes by #BernieWhite.
36+
[#3184](https://github.com/Azure/PSRule.Rules.Azure/issues/3184)
37+
3238
## v1.40.0-B0147 (pre-release)
3339

3440
What's changed since pre-release v1.40.0-B0103:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Xml;
5+
using System.Text.RegularExpressions;
6+
using System.IO;
7+
8+
namespace PSRule.Rules.Azure.Data.APIM;
9+
10+
/// <summary>
11+
/// A reader for APIM policy files.
12+
/// </summary>
13+
internal static class APIMPolicyReader
14+
{
15+
// Create a regex pattern to match the `="@(` `)"` pairs.
16+
private static readonly Regex _Pattern = new(@"(?<=\=""\@\().*?(?=\)"")", RegexOptions.Singleline | RegexOptions.Compiled);
17+
18+
/// <summary>
19+
/// Read the content of the policy file as XML.
20+
/// </summary>
21+
/// <remarks>
22+
/// This method automatically escapes quotes within policy expressions to ensure the XML is well-formed.
23+
/// </remarks>
24+
public static XmlReader ReadContent(string content)
25+
{
26+
// For each match replace `"` characters with `&quot;`.
27+
foreach (Match match in _Pattern.Matches(content))
28+
{
29+
content = content.Replace(match.Value, match.Value.Replace("\"", "&quot;"));
30+
}
31+
32+
return XmlReader.Create(new StringReader(content));
33+
}
34+
}

src/PSRule.Rules.Azure/Runtime/Helper.cs

+13
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
using System.IO;
66
using System.Linq;
77
using System.Management.Automation;
8+
using System.Xml;
89
using PSRule.Rules.Azure.Configuration;
910
using PSRule.Rules.Azure.Data;
11+
using PSRule.Rules.Azure.Data.APIM;
1012
using PSRule.Rules.Azure.Data.Bicep;
1113
using PSRule.Rules.Azure.Data.Network;
1214
using PSRule.Rules.Azure.Data.Template;
@@ -206,6 +208,17 @@ public static string GetSubResourceName(string resourceName)
206208
return parts[parts.Length - 1];
207209
}
208210

211+
/// <summary>
212+
/// Load a APIM policy as an XML document.
213+
/// </summary>
214+
public static XmlDocument GetAPIMPolicyDocument(string content)
215+
{
216+
using var reader = APIMPolicyReader.ReadContent(content);
217+
var doc = new XmlDocument();
218+
doc.Load(reader);
219+
return doc;
220+
}
221+
209222
#region Helper methods
210223

211224
private static PSObject[] GetTemplateResources(string templateFile, string parameterFile, PipelineContext context)

src/PSRule.Rules.Azure/rules/Azure.APIM.Rule.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ function global:GetAPIMPolicyNode {
383383
}
384384
$policies | ForEach-Object {
385385
if (!($IgnoreGlobal -and $_.type -eq 'Microsoft.ApiManagement/service/policies') -and $_.properties.format -in 'rawxml', 'xml' -and $_.properties.value) {
386-
$xml = [Xml]$_.properties.value
386+
$xml = [PSRule.Rules.Azure.Runtime.Helper]::GetAPIMPolicyDocument($_.properties.value)
387387
$xml.SelectNodes("//${Node}")
388388
}
389389
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Xml;
5+
using PSRule.Rules.Azure.Data.APIM;
6+
7+
namespace PSRule.Rules.Azure.APIM;
8+
9+
/// <summary>
10+
/// Test cases for APIM policy files with complex syntax.
11+
/// </summary>
12+
public sealed class APIMPolicyTests : BaseTests
13+
{
14+
/// <summary>
15+
/// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3184
16+
/// </summary>
17+
[Fact]
18+
public void APIMPolicyReader_WhenExpressionIsFound_ShouldEscapeAndLoad()
19+
{
20+
using var reader = ReadContentFromFile("APIM/Tests.Policy.1.xml");
21+
22+
var doc = new XmlDocument();
23+
doc.Load(reader);
24+
25+
var node = doc.SelectSingleNode("//set-variable");
26+
27+
Assert.Equal("@(context.Request.Headers.GetValueOrDefault(\"X-Original-Host\", \"NotAvailable\"))", node.Attributes["value"].Value);
28+
}
29+
30+
#region Helper methods
31+
32+
private static XmlReader ReadContentFromFile(string fileName)
33+
{
34+
var content = GetContent(fileName);
35+
return APIMPolicyReader.ReadContent(content);
36+
}
37+
38+
#endregion Helper methods
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!-- Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3184 -->
2+
<!-- Based on work contributed by @GABRIELNGBTUC -->
3+
<policies>
4+
<inbound>
5+
<base />
6+
<set-backend-service base-url="{{backend}}" />
7+
<authentication-managed-identity resource="{{audience}}" />
8+
<set-variable name="originalHost" value="@(context.Request.Headers.GetValueOrDefault("X-Original-Host", "NotAvailable"))" />
9+
</inbound>
10+
<backend>
11+
<base />
12+
</backend>
13+
<outbound>
14+
<base />
15+
</outbound>
16+
<on-error>
17+
<base />
18+
</on-error>
19+
</policies>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.IO;
6+
7+
namespace PSRule.Rules.Azure;
8+
9+
public abstract class BaseTests
10+
{
11+
protected static string GetSourcePath(string fileName)
12+
{
13+
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
14+
}
15+
16+
protected static string GetContent(string fileName)
17+
{
18+
var path = GetSourcePath(fileName);
19+
return File.ReadAllText(path);
20+
}
21+
}

tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@
317317
<None Update="Bicep\ScopeTestCases\Tests.Bicep.1.json">
318318
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
319319
</None>
320+
<None Update="APIM\Tests.Policy.1.xml">
321+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
322+
</None>
320323
</ItemGroup>
321324

322325
</Project>

tests/PSRule.Rules.Azure.Tests/TemplateVisitorTestsBase.cs

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System;
5-
using System.IO;
64
using System.Linq;
75
using Newtonsoft.Json.Linq;
86
using PSRule.Rules.Azure.Configuration;
@@ -12,15 +10,10 @@
1210

1311
namespace PSRule.Rules.Azure;
1412

15-
public abstract class TemplateVisitorTestsBase
13+
public abstract class TemplateVisitorTestsBase : BaseTests
1614
{
1715
#region Helper methods
1816

19-
protected static string GetSourcePath(string fileName)
20-
{
21-
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
22-
}
23-
2417
protected static JObject[] ProcessTemplate(string templateFile, string parametersFile)
2518
{
2619
var context = new PipelineContext(PSRuleOption.Default, null);

0 commit comments

Comments
 (0)