Skip to content

Commit 2c3e973

Browse files
authored
Fixes cast with union array and mock Azure#2614 (Azure#2615)
1 parent 44118b9 commit 2c3e973

11 files changed

+251
-8
lines changed

bicepconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"experimentalFeaturesEnabled": {
3-
"userDefinedTypes": true
3+
"userDefinedTypes": true,
4+
"optionalModuleNames": true
45
}
56
}

docs/CHANGELOG-v1.md

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ What's changed since v1.32.0:
3939
[#2593](https://github.com/Azure/PSRule.Rules.Azure/issues/2593)
4040
- Fixed failure to expand copy loop in a Azure Policy deployment by @BernieWhite.
4141
[#2605](https://github.com/Azure/PSRule.Rules.Azure/issues/2605)
42+
- Fixed cast exception when expanding the union of an array and mock by @BernieWhite.
43+
[#2614](https://github.com/Azure/PSRule.Rules.Azure/issues/2614)
4244

4345
## v1.32.0
4446

src/PSRule.Rules.Azure/Data/Template/ExpressionHelpers.cs

+24-2
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,8 @@ internal static object UnionArray(object[] o)
585585
{
586586
for (var j = 0; j < jArray.Count; j++)
587587
{
588-
var element = jArray[j];
589-
if (!result.Contains(element))
588+
var element = GetBaseObject(jArray[j]);
589+
if (element != null && !result.Contains(element))
590590
result.Add(element);
591591
}
592592
}
@@ -612,6 +612,28 @@ internal static object UnionArray(object[] o)
612612
return result.ToArray();
613613
}
614614

615+
private static object GetBaseObject(JToken token)
616+
{
617+
object result = token;
618+
if (token.Type == JTokenType.String)
619+
{
620+
result = token.Value<string>();
621+
}
622+
else if (token.Type == JTokenType.Integer)
623+
{
624+
result = token.Value<long>();
625+
}
626+
else if (token.Type == JTokenType.Boolean)
627+
{
628+
result = token.Value<bool>();
629+
}
630+
else if (token.Type == JTokenType.Null)
631+
{
632+
result = null;
633+
}
634+
return result;
635+
}
636+
615637
internal static bool IsObject(object o)
616638
{
617639
return o is JObject or

src/PSRule.Rules.Azure/Data/Template/Functions.cs

+20-2
Original file line numberDiff line numberDiff line change
@@ -717,14 +717,30 @@ internal static object TryGet(ITemplateContext context, object[] args)
717717
return o;
718718
}
719719

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

725-
// Find first non-null case
733+
var hasMocks = false;
734+
735+
// Find first non-null case.
726736
for (var i = 0; i < args.Length; i++)
727737
{
738+
if (args[i] is IMock)
739+
{
740+
hasMocks = true;
741+
continue;
742+
}
743+
728744
// Array
729745
if (ExpressionHelpers.IsArray(args[i]))
730746
return ExpressionHelpers.UnionArray(args);
@@ -733,7 +749,9 @@ internal static object Union(ITemplateContext context, object[] args)
733749
if (ExpressionHelpers.IsObject(args[i]))
734750
return ExpressionHelpers.UnionObject(args);
735751
}
736-
return null;
752+
753+
// Handle mocks as objects if no other object or array is found.
754+
return hasMocks ? ExpressionHelpers.UnionObject(args) : null;
737755
}
738756

739757
#endregion Array and object

src/PSRule.Rules.Azure/Data/Template/RuleDataExportVisitor.cs

+26-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal sealed class RuleDataExportVisitor : TemplateVisitor
3434
private const string PROPERTY_LOGINSERVER = "loginServer";
3535
private const string PROPERTY_RULES = "rules";
3636
private const string PROPERTY_RULEID = "ruleId";
37+
private const string PROPERTY_ACCESSPOLICIES = "accessPolicies";
3738

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

5557
private static readonly JsonMergeSettings _MergeSettings = new()
@@ -128,7 +130,8 @@ private static void ProjectRuntimeProperties(TemplateContext context, IResourceV
128130
ProjectContainerRegistry(context, resource) ||
129131
ProjectPrivateEndpoints(context, resource) ||
130132
ProjectSubscriptionAlias(context, resource) ||
131-
StorageObjectReplicationPolicies(context, resource) ||
133+
ProjectStorageObjectReplicationPolicies(context, resource) ||
134+
ProjectKeyVault(context, resource) ||
132135
ProjectResource(context, resource);
133136
}
134137

@@ -246,10 +249,31 @@ private static bool ProjectContainerRegistry(TemplateContext context, IResourceV
246249
{
247250
properties[PROPERTY_LOGINSERVER] = $"{resource.Name}.azurecr.io";
248251
}
252+
return ProjectResource(context, resource);
253+
}
254+
255+
private static bool ProjectKeyVault(TemplateContext context, IResourceValue resource)
256+
{
257+
if (!resource.IsType(TYPE_KEYVAULT))
258+
return false;
259+
260+
resource.Value.UseProperty(PROPERTY_PROPERTIES, out JObject properties);
261+
262+
// Add properties.accessPolicies
263+
if (!properties.ContainsKeyInsensitive(PROPERTY_ACCESSPOLICIES))
264+
{
265+
properties[PROPERTY_ACCESSPOLICIES] = new JArray();
266+
}
267+
268+
// Add properties.tenantId
269+
if (!properties.ContainsKeyInsensitive(PROPERTY_TENANTID))
270+
{
271+
properties[PROPERTY_TENANTID] = context.Tenant.TenantId;
272+
}
249273
return true;
250274
}
251275

252-
private static bool StorageObjectReplicationPolicies(TemplateContext context, IResourceValue resource)
276+
private static bool ProjectStorageObjectReplicationPolicies(TemplateContext context, IResourceValue resource)
253277
{
254278
if (!resource.IsType(TYPE_STORAGE_OBJECTREPLICATIONPOLICIES))
255279
return false;

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

+4
Original file line numberDiff line numberDiff line change
@@ -575,10 +575,14 @@ public void Union()
575575
Assert.Equal(2, actual2.Length);
576576
actual2 = Functions.Union(context, new object[] { new string[] { "one", "two" }, null, new string[] { "one", "three" } }) as object[];
577577
Assert.Equal(3, actual2.Length);
578+
actual2 = Functions.Union(context, new object[] { new string[] { "one", "two" }, null, new JArray { "one", "three" } }) as object[];
579+
Assert.Equal(3, actual2.Length);
578580
actual2 = Functions.Union(context, new object[] { new string[] { "one", "two" }, new Mock.MockArray() }) as object[];
579581
Assert.Equal(2, actual2.Length);
580582
actual2 = Functions.Union(context, new object[] { null, new string[] { "three", "four" } }) as object[];
581583
Assert.Equal(2, actual2.Length);
584+
actual2 = Functions.Union(context, new object[] { new Mock.MockUnknownObject(), new JArray { "one", "two" } }) as object[];
585+
Assert.Equal(2, actual2.Length);
582586
}
583587

584588
[Fact]

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

+3
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@
218218
<None Update="Tests.Bicep.33.json">
219219
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
220220
</None>
221+
<None Update="Tests.Bicep.34.json">
222+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
223+
</None>
221224
<None Update="Tests.Bicep.4.json">
222225
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
223226
</None>

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -1060,10 +1060,25 @@ public void Quoting()
10601060
[Fact]
10611061
public void PolicyCopyLoop()
10621062
{
1063-
var resources = ProcessTemplate(GetSourcePath("Template.Policy.WithDeployment.json"), null, out var templateContext);
1063+
var resources = ProcessTemplate(GetSourcePath("Template.Policy.WithDeployment.json"), null, out _);
10641064
Assert.Equal(2, resources.Length);
10651065
}
10661066

1067+
[Fact]
1068+
public void UnionMockWithArray()
1069+
{
1070+
var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.34.json"), null, out _);
1071+
Assert.Equal(4, resources.Length);
1072+
1073+
var actual = resources[1];
1074+
Assert.Equal("Microsoft.KeyVault/vaults", actual["type"].Value<string>());
1075+
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual["properties"]["tenantId"].Value<string>());
1076+
Assert.Empty(actual["properties"]["accessPolicies"].Value<JArray>());
1077+
1078+
actual = resources[3];
1079+
Assert.Equal("Microsoft.KeyVault/vaults/accessPolicies", actual["type"].Value<string>());
1080+
}
1081+
10671082
#region Helper methods
10681083

10691084
private static string GetSourcePath(string fileName)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
param keyVaultName string = 'vault1'
5+
param objectId string = newGuid()
6+
7+
var newAccessPolicies = [
8+
{
9+
tenantId: tenant().tenantId
10+
objectId: objectId
11+
permissions: {
12+
keys: [
13+
'Get'
14+
'List'
15+
]
16+
secrets: [
17+
'Get'
18+
'List'
19+
]
20+
certificates: []
21+
}
22+
}
23+
]
24+
25+
resource keyvault 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = {
26+
name: keyVaultName
27+
}
28+
29+
#disable-next-line BCP035
30+
resource keyvault2 'Microsoft.KeyVault/vaults@2023-07-01' = {
31+
name: '${keyVaultName}-2'
32+
33+
#disable-next-line BCP035
34+
properties: {}
35+
}
36+
37+
var existingAccessPolicies = keyvault.properties.accessPolicies
38+
var allPolicies = union(existingAccessPolicies, newAccessPolicies)
39+
40+
module addPolicies './Tests.Bicep.34.child.bicep' = {
41+
params: {
42+
accessPolicies: allPolicies
43+
keyVaultName: keyVaultName
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
param keyVaultName string
5+
param accessPolicies array
6+
7+
// Policies to add.
8+
resource additionalPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2023-07-01' = {
9+
name: '${keyVaultName}/add'
10+
properties: {
11+
accessPolicies: accessPolicies
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
3+
"contentVersion": "1.0.0.0",
4+
"metadata": {
5+
"_generator": {
6+
"name": "bicep",
7+
"version": "0.24.24.22086",
8+
"templateHash": "13351406772210248604"
9+
}
10+
},
11+
"parameters": {
12+
"keyVaultName": {
13+
"type": "string",
14+
"defaultValue": "vault1"
15+
},
16+
"objectId": {
17+
"type": "string",
18+
"defaultValue": "[newGuid()]"
19+
}
20+
},
21+
"variables": {
22+
"newAccessPolicies": [
23+
{
24+
"tenantId": "[tenant().tenantId]",
25+
"objectId": "[parameters('objectId')]",
26+
"permissions": {
27+
"keys": [
28+
"Get",
29+
"List"
30+
],
31+
"secrets": [
32+
"Get",
33+
"List"
34+
],
35+
"certificates": []
36+
}
37+
}
38+
]
39+
},
40+
"resources": [
41+
{
42+
"type": "Microsoft.KeyVault/vaults",
43+
"apiVersion": "2023-07-01",
44+
"name": "[format('{0}-2', parameters('keyVaultName'))]",
45+
"properties": {}
46+
},
47+
{
48+
"type": "Microsoft.Resources/deployments",
49+
"apiVersion": "2022-09-01",
50+
"name": "[format('addPolicies-{0}', uniqueString('addPolicies', deployment().name))]",
51+
"properties": {
52+
"expressionEvaluationOptions": {
53+
"scope": "inner"
54+
},
55+
"mode": "Incremental",
56+
"parameters": {
57+
"accessPolicies": {
58+
"value": "[union(reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2021-11-01-preview').accessPolicies, variables('newAccessPolicies'))]"
59+
},
60+
"keyVaultName": {
61+
"value": "[parameters('keyVaultName')]"
62+
}
63+
},
64+
"template": {
65+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
66+
"contentVersion": "1.0.0.0",
67+
"metadata": {
68+
"_generator": {
69+
"name": "bicep",
70+
"version": "0.24.24.22086",
71+
"templateHash": "11282630508739439881"
72+
}
73+
},
74+
"parameters": {
75+
"keyVaultName": {
76+
"type": "string"
77+
},
78+
"accessPolicies": {
79+
"type": "array"
80+
}
81+
},
82+
"resources": [
83+
{
84+
"type": "Microsoft.KeyVault/vaults/accessPolicies",
85+
"apiVersion": "2023-07-01",
86+
"name": "[format('{0}/add', parameters('keyVaultName'))]",
87+
"properties": {
88+
"accessPolicies": "[parameters('accessPolicies')]"
89+
}
90+
}
91+
]
92+
}
93+
}
94+
}
95+
]
96+
}

0 commit comments

Comments
 (0)