Skip to content

Commit

Permalink
Fixes cast with union array and mock #2614 (#2615)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Dec 18, 2023
1 parent 44118b9 commit 2c3e973
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 8 deletions.
3 changes: 2 additions & 1 deletion bicepconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"experimentalFeaturesEnabled": {
"userDefinedTypes": true
"userDefinedTypes": true,
"optionalModuleNames": true
}
}
2 changes: 2 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ What's changed since v1.32.0:
[#2593](https://github.com/Azure/PSRule.Rules.Azure/issues/2593)
- Fixed failure to expand copy loop in a Azure Policy deployment by @BernieWhite.
[#2605](https://github.com/Azure/PSRule.Rules.Azure/issues/2605)
- Fixed cast exception when expanding the union of an array and mock by @BernieWhite.
[#2614](https://github.com/Azure/PSRule.Rules.Azure/issues/2614)

## v1.32.0

Expand Down
26 changes: 24 additions & 2 deletions src/PSRule.Rules.Azure/Data/Template/ExpressionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,8 @@ internal static object UnionArray(object[] o)
{
for (var j = 0; j < jArray.Count; j++)
{
var element = jArray[j];
if (!result.Contains(element))
var element = GetBaseObject(jArray[j]);
if (element != null && !result.Contains(element))
result.Add(element);
}
}
Expand All @@ -612,6 +612,28 @@ internal static object UnionArray(object[] o)
return result.ToArray();
}

private static object GetBaseObject(JToken token)
{
object result = token;
if (token.Type == JTokenType.String)
{
result = token.Value<string>();
}
else if (token.Type == JTokenType.Integer)
{
result = token.Value<long>();
}
else if (token.Type == JTokenType.Boolean)
{
result = token.Value<bool>();
}
else if (token.Type == JTokenType.Null)
{
result = null;
}
return result;
}

internal static bool IsObject(object o)
{
return o is JObject or
Expand Down
22 changes: 20 additions & 2 deletions src/PSRule.Rules.Azure/Data/Template/Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -717,14 +717,30 @@ internal static object TryGet(ITemplateContext context, object[] args)
return o;
}

/// <summary>
/// union(arg1, arg2, arg3, ...)
/// </summary>
/// <remarks>
/// Returns a single array or object with all elements from the parameters. For arrays, duplicate values are included once.
/// For objects, duplicate property names are only included once.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/templates/template-functions-object#union"/>.
/// </remarks>
internal static object Union(ITemplateContext context, object[] args)
{
if (CountArgs(args) < 2)
throw ArgumentsOutOfRange(nameof(Union), args);

// Find first non-null case
var hasMocks = false;

// Find first non-null case.
for (var i = 0; i < args.Length; i++)
{
if (args[i] is IMock)
{
hasMocks = true;
continue;
}

// Array
if (ExpressionHelpers.IsArray(args[i]))
return ExpressionHelpers.UnionArray(args);
Expand All @@ -733,7 +749,9 @@ internal static object Union(ITemplateContext context, object[] args)
if (ExpressionHelpers.IsObject(args[i]))
return ExpressionHelpers.UnionObject(args);
}
return null;

// Handle mocks as objects if no other object or array is found.
return hasMocks ? ExpressionHelpers.UnionObject(args) : null;
}

#endregion Array and object
Expand Down
28 changes: 26 additions & 2 deletions src/PSRule.Rules.Azure/Data/Template/RuleDataExportVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal sealed class RuleDataExportVisitor : TemplateVisitor
private const string PROPERTY_LOGINSERVER = "loginServer";
private const string PROPERTY_RULES = "rules";
private const string PROPERTY_RULEID = "ruleId";
private const string PROPERTY_ACCESSPOLICIES = "accessPolicies";

private const string PLACEHOLDER_GUID = "ffffffff-ffff-ffff-ffff-ffffffffffff";
private const string IDENTITY_SYSTEMASSIGNED = "SystemAssigned";
Expand All @@ -50,6 +51,7 @@ internal sealed class RuleDataExportVisitor : TemplateVisitor
private const string TYPE_NETWORKINTERFACE = "Microsoft.Network/networkInterfaces";
private const string TYPE_SUBSCRIPTIONALIAS = "Microsoft.Subscription/aliases";
private const string TYPE_CONTAINERREGISTRY = "Microsoft.ContainerRegistry/registries";
private const string TYPE_KEYVAULT = "Microsoft.KeyVault/vaults";
private const string TYPE_STORAGE_OBJECTREPLICATIONPOLICIES = "Microsoft.Storage/storageAccounts/objectReplicationPolicies";

private static readonly JsonMergeSettings _MergeSettings = new()
Expand Down Expand Up @@ -128,7 +130,8 @@ private static void ProjectRuntimeProperties(TemplateContext context, IResourceV
ProjectContainerRegistry(context, resource) ||
ProjectPrivateEndpoints(context, resource) ||
ProjectSubscriptionAlias(context, resource) ||
StorageObjectReplicationPolicies(context, resource) ||
ProjectStorageObjectReplicationPolicies(context, resource) ||
ProjectKeyVault(context, resource) ||
ProjectResource(context, resource);
}

Expand Down Expand Up @@ -246,10 +249,31 @@ private static bool ProjectContainerRegistry(TemplateContext context, IResourceV
{
properties[PROPERTY_LOGINSERVER] = $"{resource.Name}.azurecr.io";
}
return ProjectResource(context, resource);
}

private static bool ProjectKeyVault(TemplateContext context, IResourceValue resource)
{
if (!resource.IsType(TYPE_KEYVAULT))
return false;

resource.Value.UseProperty(PROPERTY_PROPERTIES, out JObject properties);

// Add properties.accessPolicies
if (!properties.ContainsKeyInsensitive(PROPERTY_ACCESSPOLICIES))
{
properties[PROPERTY_ACCESSPOLICIES] = new JArray();
}

// Add properties.tenantId
if (!properties.ContainsKeyInsensitive(PROPERTY_TENANTID))
{
properties[PROPERTY_TENANTID] = context.Tenant.TenantId;
}
return true;
}

private static bool StorageObjectReplicationPolicies(TemplateContext context, IResourceValue resource)
private static bool ProjectStorageObjectReplicationPolicies(TemplateContext context, IResourceValue resource)
{
if (!resource.IsType(TYPE_STORAGE_OBJECTREPLICATIONPOLICIES))
return false;
Expand Down
4 changes: 4 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/FunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,10 +575,14 @@ public void Union()
Assert.Equal(2, actual2.Length);
actual2 = Functions.Union(context, new object[] { new string[] { "one", "two" }, null, new string[] { "one", "three" } }) as object[];
Assert.Equal(3, actual2.Length);
actual2 = Functions.Union(context, new object[] { new string[] { "one", "two" }, null, new JArray { "one", "three" } }) as object[];
Assert.Equal(3, actual2.Length);
actual2 = Functions.Union(context, new object[] { new string[] { "one", "two" }, new Mock.MockArray() }) as object[];
Assert.Equal(2, actual2.Length);
actual2 = Functions.Union(context, new object[] { null, new string[] { "three", "four" } }) as object[];
Assert.Equal(2, actual2.Length);
actual2 = Functions.Union(context, new object[] { new Mock.MockUnknownObject(), new JArray { "one", "two" } }) as object[];
Assert.Equal(2, actual2.Length);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@
<None Update="Tests.Bicep.33.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Tests.Bicep.34.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Tests.Bicep.4.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
17 changes: 16 additions & 1 deletion tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1060,10 +1060,25 @@ public void Quoting()
[Fact]
public void PolicyCopyLoop()
{
var resources = ProcessTemplate(GetSourcePath("Template.Policy.WithDeployment.json"), null, out var templateContext);
var resources = ProcessTemplate(GetSourcePath("Template.Policy.WithDeployment.json"), null, out _);
Assert.Equal(2, resources.Length);
}

[Fact]
public void UnionMockWithArray()
{
var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.34.json"), null, out _);
Assert.Equal(4, resources.Length);

var actual = resources[1];
Assert.Equal("Microsoft.KeyVault/vaults", actual["type"].Value<string>());
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual["properties"]["tenantId"].Value<string>());
Assert.Empty(actual["properties"]["accessPolicies"].Value<JArray>());

actual = resources[3];
Assert.Equal("Microsoft.KeyVault/vaults/accessPolicies", actual["type"].Value<string>());
}

#region Helper methods

private static string GetSourcePath(string fileName)
Expand Down
45 changes: 45 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Tests.Bicep.34.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

param keyVaultName string = 'vault1'
param objectId string = newGuid()

var newAccessPolicies = [
{
tenantId: tenant().tenantId
objectId: objectId
permissions: {
keys: [
'Get'
'List'
]
secrets: [
'Get'
'List'
]
certificates: []
}
}
]

resource keyvault 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = {
name: keyVaultName
}

#disable-next-line BCP035
resource keyvault2 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: '${keyVaultName}-2'

#disable-next-line BCP035
properties: {}
}

var existingAccessPolicies = keyvault.properties.accessPolicies
var allPolicies = union(existingAccessPolicies, newAccessPolicies)

module addPolicies './Tests.Bicep.34.child.bicep' = {
params: {
accessPolicies: allPolicies
keyVaultName: keyVaultName
}
}
13 changes: 13 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Tests.Bicep.34.child.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

param keyVaultName string
param accessPolicies array

// Policies to add.
resource additionalPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2023-07-01' = {
name: '${keyVaultName}/add'
properties: {
accessPolicies: accessPolicies
}
}
96 changes: 96 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Tests.Bicep.34.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.24.24.22086",
"templateHash": "13351406772210248604"
}
},
"parameters": {
"keyVaultName": {
"type": "string",
"defaultValue": "vault1"
},
"objectId": {
"type": "string",
"defaultValue": "[newGuid()]"
}
},
"variables": {
"newAccessPolicies": [
{
"tenantId": "[tenant().tenantId]",
"objectId": "[parameters('objectId')]",
"permissions": {
"keys": [
"Get",
"List"
],
"secrets": [
"Get",
"List"
],
"certificates": []
}
}
]
},
"resources": [
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2023-07-01",
"name": "[format('{0}-2', parameters('keyVaultName'))]",
"properties": {}
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2022-09-01",
"name": "[format('addPolicies-{0}', uniqueString('addPolicies', deployment().name))]",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"parameters": {
"accessPolicies": {
"value": "[union(reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2021-11-01-preview').accessPolicies, variables('newAccessPolicies'))]"
},
"keyVaultName": {
"value": "[parameters('keyVaultName')]"
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.24.24.22086",
"templateHash": "11282630508739439881"
}
},
"parameters": {
"keyVaultName": {
"type": "string"
},
"accessPolicies": {
"type": "array"
}
},
"resources": [
{
"type": "Microsoft.KeyVault/vaults/accessPolicies",
"apiVersion": "2023-07-01",
"name": "[format('{0}/add', parameters('keyVaultName'))]",
"properties": {
"accessPolicies": "[parameters('accessPolicies')]"
}
}
]
}
}
}
]
}

0 comments on commit 2c3e973

Please sign in to comment.