From 5f7b0bf3ddfceca3a1879452b1b9dcf91d7621f3 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 4 Nov 2023 16:43:06 +1000 Subject: [PATCH] Fixed API Connection might be missing dynamic properties #2424 (#2517) --- docs/CHANGELOG-v1.md | 2 + src/PSRule.Rules.Azure/Data/Template/Mocks.cs | 51 ++++- .../PSRule.Rules.Azure.Tests.csproj | 3 + .../TemplateVisitorTests.cs | 18 ++ .../Tests.Bicep.29.bicep | 44 ++++ .../Tests.Bicep.29.child.bicep | 86 ++++++++ .../Tests.Bicep.29.json | 188 ++++++++++++++++++ 7 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.bicep create mode 100644 tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.child.bicep create mode 100644 tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.json diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 6e36489daff..37237fb8d66 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -40,6 +40,8 @@ What's changed since pre-release v1.31.0-B0020: [#2505](https://github.com/Azure/PSRule.Rules.Azure/issues/2505) - Fixed nullable parameters for custom types by @BernieWhite. [#2489](https://github.com/Azure/PSRule.Rules.Azure/issues/2489) + - Fixed API Connection might be missing dynamic properties by @BernieWhite. + [#2424](https://github.com/Azure/PSRule.Rules.Azure/issues/2424) ## v1.31.0-B0020 (pre-release) diff --git a/src/PSRule.Rules.Azure/Data/Template/Mocks.cs b/src/PSRule.Rules.Azure/Data/Template/Mocks.cs index df46146bc5b..94988345e96 100644 --- a/src/PSRule.Rules.Azure/Data/Template/Mocks.cs +++ b/src/PSRule.Rules.Azure/Data/Template/Mocks.cs @@ -173,7 +173,13 @@ public TValue GetValue() public JToken GetValue(TypePrimitive type) { - throw new NotImplementedException(); + if (type == TypePrimitive.None || BaseType == type) + return this; + + if (TryMutate(type, this, out var value)) + return value; + + return null; } public JToken GetValue(object key) @@ -315,7 +321,12 @@ public virtual JToken GetValue(object key) var result = base[key]; if (result == null) { - result = new MockUnknownObject(IsSecret); + // Guess string type for common scenarios. + if (key is string s_key && TryWellKnownStringProperty(this, s_key, out var s_value)) + result = s_value; + else + result = new MockUnknownObject(IsSecret); + base[key] = result; } else if (result is JObject jObject) @@ -464,5 +475,41 @@ private static TypePrimitive GetTypePrimitive(JToken token) throw new NotImplementedException(); } + + private static bool TryWellKnownStringProperty(JObject o, string key, out JValue value) + { + value = default; + if (StringComparer.OrdinalIgnoreCase.Equals(key, "primaryConnectionString") || + StringComparer.OrdinalIgnoreCase.Equals(key, "secondaryConnectionString")) + { + value = new JValue(key); + return true; + } + + // Handle name and type + if (StringComparer.OrdinalIgnoreCase.Equals(key, "name")) + { + value = TryExpandId(o) && o.TryGetProperty("name", out var nameValue) ? nameValue : new JValue(key); + return true; + } + else if (StringComparer.OrdinalIgnoreCase.Equals(key, "type") && TryExpandId(o)) + { + value = o.TryGetProperty("type", out var nameValue) ? nameValue : default; + return true; + } + return false; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Don't convert to var to ignore ambiguous type reference.")] + private static bool TryExpandId(JObject o) + { + if (o.TryGetProperty("id", out var id) && ResourceHelper.TryResourceIdComponents(id, out _, out _, out string resourceType, out string resourceName)) + { + o["name"] = resourceName; + o["type"] = resourceType; + return true; + } + return false; + } } } diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj index 51cc32c9878..67bd33f66a4 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -200,6 +200,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs index 38cfe0bb811..0d8a38c0417 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs @@ -914,6 +914,24 @@ public void NullableParameters() Assert.Empty(actual["resources"][0]["properties"]["cors"]["corsRules"].Value()); } + [Fact] + public void MockWellKnownProperties() + { + var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.29.json"), null, out _); + Assert.Equal(6, resources.Length); + + var actual = resources[2]; + Assert.Equal("Microsoft.ServiceBus/namespaces", actual["type"].Value()); + + actual = resources[3]; + Assert.Equal("Microsoft.Web/connections", actual["type"].Value()); + Assert.Equal("/subscriptions/ffffffff-ffff-ffff-ffff-ffffffffffff/providers/Microsoft.Web/locations/eastus/managedApis/servicebus", actual["properties"]["api"]["id"].Value()); + + actual = resources[4]; + Assert.Equal("Microsoft.Web/connections", actual["type"].Value()); + Assert.Equal("/subscriptions/ffffffff-ffff-ffff-ffff-ffffffffffff/providers/Microsoft.Web/locations/eastus/managedApis/servicebus", actual["properties"]["api"]["id"].Value()); + } + #region Helper methods private static string GetSourcePath(string fileName) diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.bicep new file mode 100644 index 00000000000..de4c35e6843 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.bicep @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Community provided sample from: https://github.com/Azure/PSRule.Rules.Azure/issues/2424 + +#disable-next-line no-hardcoded-location +var location = 'eastus' + +module serviceBus './Tests.Bicep.29.child.bicep' = { + name: 'my-test-service-bus' + params: { + Location: location + ServiceBusSkuName: 'Standard' + } +} + +var logicAppParams = { + '$connections': { + value: { + serviceBusSender: { + connectionId: serviceBus.outputs.SendConnection.Id + connectionName: serviceBus.outputs.SendConnection.Name + id: subscriptionResourceId(serviceBus.outputs.SendConnection.Api.Type, location, serviceBus.outputs.SendConnection.Api.Name) + } + serviceBusListener: { + connectionId: serviceBus.outputs.ListenConnection.Id + connectionName: serviceBus.outputs.ListenConnection.Name + id: subscriptionResourceId(serviceBus.outputs.ListenConnection.Api.Type, location, serviceBus.outputs.ListenConnection.Api.Name) + } + } + } +} + +resource workflow 'Microsoft.Logic/workflows@2019-05-01' = { + name: 'a-test-logic-app' + location: location + properties: { + state: 'Enabled' + definition: { + // Actual definition omitted + } + parameters: logicAppParams + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.child.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.child.bicep new file mode 100644 index 00000000000..288cc4e2a00 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.child.bicep @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Community provided sample from: https://github.com/Azure/PSRule.Rules.Azure/issues/2424 + +param Location string = resourceGroup().location +param ServiceBusSkuName string = 'Standard' + +resource serviceBus 'Microsoft.ServiceBus/namespaces@2022-01-01-preview' = { + name: 'myServiceBus' + location: Location + sku: { + name: ServiceBusSkuName + tier: ServiceBusSkuName + } + properties: { + minimumTlsVersion: '1.2' + } + + resource sendAuthRule 'AuthorizationRules' = { + name: 'SendAccess' + properties: { + rights: [ + 'Send' + ] + } + } + + resource listenAuthRule 'AuthorizationRules' = { + name: 'ListenAccess' + properties: { + rights: [ + 'Listen' + ] + } + } + + // Queues and Topics omitted + +} + +resource sendApiConnection 'Microsoft.Web/connections@2016-06-01' = { + name: 'sender-connection' + location: Location + properties: { + api: { + id: subscriptionResourceId('Microsoft.Web/locations/managedApis', Location, 'servicebus') + } + displayName: 'sender-connection' + parameterValues: { + connectionString: listKeys(serviceBus::sendAuthRule.name, serviceBus.apiVersion).primaryConnectionString + } + } +} + +resource listenApiConnection 'Microsoft.Web/connections@2016-06-01' = { + name: 'listener-connection' + location: Location + properties: { + api: { + id: subscriptionResourceId('Microsoft.Web/locations/managedApis', Location, 'servicebus') + } + displayName: 'listener-connection' + parameterValues: { + connectionString: listKeys(serviceBus::listenAuthRule.name, serviceBus.apiVersion).primaryConnectionString + } + } +} + +output SendConnection object = { + Name: sendApiConnection.name + Id: sendApiConnection.id + Api: { + Name: sendApiConnection.properties.api.name + Type: sendApiConnection.properties.api.type + } +} + +output ListenConnection object = { + Name: listenApiConnection.name + Id: listenApiConnection.id + Api: { + Name: listenApiConnection.properties.api.name + Type: listenApiConnection.properties.api.type + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.json new file mode 100644 index 00000000000..e390413b9be --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.29.json @@ -0,0 +1,188 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "4000541406517808732" + } + }, + "variables": { + "location": "eastus" + }, + "resources": [ + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2019-05-01", + "name": "a-test-logic-app", + "location": "[variables('location')]", + "properties": { + "state": "Enabled", + "definition": {}, + "parameters": { + "$connections": { + "value": { + "serviceBusSender": { + "connectionId": "[reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.SendConnection.value.Id]", + "connectionName": "[reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.SendConnection.value.Name]", + "id": "[subscriptionResourceId(reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.SendConnection.value.Api.Type, variables('location'), reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.SendConnection.value.Api.Name)]" + }, + "serviceBusListener": { + "connectionId": "[reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.ListenConnection.value.Id]", + "connectionName": "[reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.ListenConnection.value.Name]", + "id": "[subscriptionResourceId(reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.ListenConnection.value.Api.Type, variables('location'), reference(resourceId('Microsoft.Resources/deployments', 'my-test-service-bus'), '2022-09-01').outputs.ListenConnection.value.Api.Name)]" + } + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'my-test-service-bus')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "my-test-service-bus", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "Location": { + "value": "[variables('location')]" + }, + "ServiceBusSkuName": { + "value": "Standard" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "9687299888791726771" + } + }, + "parameters": { + "Location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "ServiceBusSkuName": { + "type": "string", + "defaultValue": "Standard" + } + }, + "resources": [ + { + "type": "Microsoft.ServiceBus/namespaces/AuthorizationRules", + "apiVersion": "2022-01-01-preview", + "name": "[format('{0}/{1}', 'myServiceBus', 'SendAccess')]", + "properties": { + "rights": [ + "Send" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', 'myServiceBus')]" + ] + }, + { + "type": "Microsoft.ServiceBus/namespaces/AuthorizationRules", + "apiVersion": "2022-01-01-preview", + "name": "[format('{0}/{1}', 'myServiceBus', 'ListenAccess')]", + "properties": { + "rights": [ + "Listen" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', 'myServiceBus')]" + ] + }, + { + "type": "Microsoft.ServiceBus/namespaces", + "apiVersion": "2022-01-01-preview", + "name": "myServiceBus", + "location": "[parameters('Location')]", + "sku": { + "name": "[parameters('ServiceBusSkuName')]", + "tier": "[parameters('ServiceBusSkuName')]" + }, + "properties": { + "minimumTlsVersion": "1.2" + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "sender-connection", + "location": "[parameters('Location')]", + "properties": { + "api": { + "id": "[subscriptionResourceId('Microsoft.Web/locations/managedApis', parameters('Location'), 'servicebus')]" + }, + "displayName": "sender-connection", + "parameterValues": { + "connectionString": "[listKeys('SendAccess', '2022-01-01-preview').primaryConnectionString]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces/AuthorizationRules', 'myServiceBus', 'SendAccess')]", + "[resourceId('Microsoft.ServiceBus/namespaces', 'myServiceBus')]" + ] + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "listener-connection", + "location": "[parameters('Location')]", + "properties": { + "api": { + "id": "[subscriptionResourceId('Microsoft.Web/locations/managedApis', parameters('Location'), 'servicebus')]" + }, + "displayName": "listener-connection", + "parameterValues": { + "connectionString": "[listKeys('ListenAccess', '2022-01-01-preview').primaryConnectionString]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces/AuthorizationRules', 'myServiceBus', 'ListenAccess')]", + "[resourceId('Microsoft.ServiceBus/namespaces', 'myServiceBus')]" + ] + } + ], + "outputs": { + "SendConnection": { + "type": "object", + "value": { + "Name": "sender-connection", + "Id": "[resourceId('Microsoft.Web/connections', 'sender-connection')]", + "Api": { + "Name": "[reference(resourceId('Microsoft.Web/connections', 'sender-connection'), '2016-06-01').api.name]", + "Type": "[reference(resourceId('Microsoft.Web/connections', 'sender-connection'), '2016-06-01').api.type]" + } + } + }, + "ListenConnection": { + "type": "object", + "value": { + "Name": "listener-connection", + "Id": "[resourceId('Microsoft.Web/connections', 'listener-connection')]", + "Api": { + "Name": "[reference(resourceId('Microsoft.Web/connections', 'listener-connection'), '2016-06-01').api.name]", + "Type": "[reference(resourceId('Microsoft.Web/connections', 'listener-connection'), '2016-06-01').api.type]" + } + } + } + } + } + } + } + ] +} \ No newline at end of file