Skip to content

Commit

Permalink
Fixes APIM policies when using embedded C# with quotes Azure#3184 (Az…
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Nov 30, 2024
1 parent 8d81817 commit 3e2a3b7
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 9 deletions.
6 changes: 6 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

## Unreleased

What's changed since pre-release v1.40.0-B0147:

- Bug fixes:
- Fixed evaluation of APIM policies when using embedded C# with quotes by #BernieWhite.
[#3184](https://github.com/Azure/PSRule.Rules.Azure/issues/3184)

## v1.40.0-B0147 (pre-release)

What's changed since pre-release v1.40.0-B0103:
Expand Down
34 changes: 34 additions & 0 deletions src/PSRule.Rules.Azure/Data/APIM/APIMPolicyReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Xml;
using System.Text.RegularExpressions;
using System.IO;

namespace PSRule.Rules.Azure.Data.APIM;

/// <summary>
/// A reader for APIM policy files.
/// </summary>
internal static class APIMPolicyReader
{
// Create a regex pattern to match the `="@(` `)"` pairs.
private static readonly Regex _Pattern = new(@"(?<=\=""\@\().*?(?=\)"")", RegexOptions.Singleline | RegexOptions.Compiled);

/// <summary>
/// Read the content of the policy file as XML.
/// </summary>
/// <remarks>
/// This method automatically escapes quotes within policy expressions to ensure the XML is well-formed.
/// </remarks>
public static XmlReader ReadContent(string content)
{
// For each match replace `"` characters with `&quot;`.
foreach (Match match in _Pattern.Matches(content))
{
content = content.Replace(match.Value, match.Value.Replace("\"", "&quot;"));
}

return XmlReader.Create(new StringReader(content));
}
}
13 changes: 13 additions & 0 deletions src/PSRule.Rules.Azure/Runtime/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Xml;
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Data;
using PSRule.Rules.Azure.Data.APIM;
using PSRule.Rules.Azure.Data.Bicep;
using PSRule.Rules.Azure.Data.Network;
using PSRule.Rules.Azure.Data.Template;
Expand Down Expand Up @@ -206,6 +208,17 @@ public static string GetSubResourceName(string resourceName)
return parts[parts.Length - 1];
}

/// <summary>
/// Load a APIM policy as an XML document.
/// </summary>
public static XmlDocument GetAPIMPolicyDocument(string content)
{
using var reader = APIMPolicyReader.ReadContent(content);
var doc = new XmlDocument();
doc.Load(reader);
return doc;
}

#region Helper methods

private static PSObject[] GetTemplateResources(string templateFile, string parameterFile, PipelineContext context)
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule.Rules.Azure/rules/Azure.APIM.Rule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ function global:GetAPIMPolicyNode {
}
$policies | ForEach-Object {
if (!($IgnoreGlobal -and $_.type -eq 'Microsoft.ApiManagement/service/policies') -and $_.properties.format -in 'rawxml', 'xml' -and $_.properties.value) {
$xml = [Xml]$_.properties.value
$xml = [PSRule.Rules.Azure.Runtime.Helper]::GetAPIMPolicyDocument($_.properties.value)
$xml.SelectNodes("//${Node}")
}
}
Expand Down
39 changes: 39 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/APIM/APIMPolicyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Xml;
using PSRule.Rules.Azure.Data.APIM;

namespace PSRule.Rules.Azure.APIM;

/// <summary>
/// Test cases for APIM policy files with complex syntax.
/// </summary>
public sealed class APIMPolicyTests : BaseTests
{
/// <summary>
/// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3184
/// </summary>
[Fact]
public void APIMPolicyReader_WhenExpressionIsFound_ShouldEscapeAndLoad()
{
using var reader = ReadContentFromFile("APIM/Tests.Policy.1.xml");

var doc = new XmlDocument();
doc.Load(reader);

var node = doc.SelectSingleNode("//set-variable");

Assert.Equal("@(context.Request.Headers.GetValueOrDefault(\"X-Original-Host\", \"NotAvailable\"))", node.Attributes["value"].Value);
}

#region Helper methods

private static XmlReader ReadContentFromFile(string fileName)
{
var content = GetContent(fileName);
return APIMPolicyReader.ReadContent(content);
}

#endregion Helper methods
}
19 changes: 19 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/APIM/Tests.Policy.1.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3184 -->
<!-- Based on work contributed by @GABRIELNGBTUC -->
<policies>
<inbound>
<base />
<set-backend-service base-url="{{backend}}" />
<authentication-managed-identity resource="{{audience}}" />
<set-variable name="originalHost" value="@(context.Request.Headers.GetValueOrDefault("X-Original-Host", "NotAvailable"))" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
21 changes: 21 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/BaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;

namespace PSRule.Rules.Azure;

public abstract class BaseTests
{
protected static string GetSourcePath(string fileName)
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
}

protected static string GetContent(string fileName)
{
var path = GetSourcePath(fileName);
return File.ReadAllText(path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@
<None Update="Bicep\ScopeTestCases\Tests.Bicep.1.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="APIM\Tests.Policy.1.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
9 changes: 1 addition & 8 deletions tests/PSRule.Rules.Azure.Tests/TemplateVisitorTestsBase.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Configuration;
Expand All @@ -12,15 +10,10 @@

namespace PSRule.Rules.Azure;

public abstract class TemplateVisitorTestsBase
public abstract class TemplateVisitorTestsBase : BaseTests
{
#region Helper methods

protected static string GetSourcePath(string fileName)
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
}

protected static JObject[] ProcessTemplate(string templateFile, string parametersFile)
{
var context = new PipelineContext(PSRuleOption.Default, null);
Expand Down

0 comments on commit 3e2a3b7

Please sign in to comment.