From 8be300ac493c2d6291e35b4e8752cac2a3899136 Mon Sep 17 00:00:00 2001 From: Mihaly Laszlo Molnar Date: Fri, 26 Feb 2021 18:12:09 +0100 Subject: [PATCH] Add policy evaluation details related classes `PolicyViolationErrorInfo` is missing `@JsonIgnoreProperties(ignoreUnknown = true)` annotation and `ObjectMapper` wasn't configured for ignoring unknown fields. This caused an exception when Azure returned `evaluationDetails` in it's response, making impossible to handle the error gracefully. In this commit: - Configured `ObjectMapper` with `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` to avoid issues when a new field is introduced on Azure side - created `EvaluationDetails` and subsequent classes to parse the response - added `toString` implementation for related and newly created classes for easier logging - added unit test to verify the parsing --- .../microsoft/azure/EvaluationDetails.java | 25 +++ .../azure/ExpressionEvaluationDetails.java | 60 +++++ .../com/microsoft/azure/PolicyViolation.java | 19 +- .../azure/PolicyViolationErrorInfo.java | 37 ++- .../azure/CloudErrorDeserializerTests.java | 210 ++++++++++++++++++ 5 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/EvaluationDetails.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/ExpressionEvaluationDetails.java diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/EvaluationDetails.java b/azure-client-runtime/src/main/java/com/microsoft/azure/EvaluationDetails.java new file mode 100644 index 0000000000..3816cbbe23 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/EvaluationDetails.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure; + +import java.util.List; + +public class EvaluationDetails { + + private List evaluatedExpressions; + + public List getEvaluatedExpressions() { + return evaluatedExpressions; + } + + @Override + public String toString() { + return "EvaluationDetails{" + + "evaluatedExpressions=" + evaluatedExpressions + + '}'; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/ExpressionEvaluationDetails.java b/azure-client-runtime/src/main/java/com/microsoft/azure/ExpressionEvaluationDetails.java new file mode 100644 index 0000000000..d3667780ef --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/ExpressionEvaluationDetails.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure; + +import java.util.List; + +public class ExpressionEvaluationDetails { + + private String expression; + + private String expressionValue; + + private String operator; + + private String path; + + private String result; + + private List targetValue; + + public String getExpression() { + return expression; + } + + public String getExpressionValue() { + return expressionValue; + } + + public String getOperator() { + return operator; + } + + public String getPath() { + return path; + } + + public String getResult() { + return result; + } + + public List getTargetValue() { + return targetValue; + } + + @Override + public String toString() { + return "ExpressionEvaluationDetails{" + + "expression='" + expression + '\'' + + ", expressionValue='" + expressionValue + '\'' + + ", operator='" + operator + '\'' + + ", path='" + path + '\'' + + ", result='" + result + '\'' + + ", targetValue=" + targetValue + + '}'; + } +} diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolation.java b/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolation.java index fb2357d0d1..f92cb76e45 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolation.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolation.java @@ -9,6 +9,7 @@ import java.io.IOException; import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,22 +23,24 @@ public class PolicyViolation extends TypedErrorInfo { * Policy violation error details. */ private PolicyViolationErrorInfo policyErrorInfo; - + /** * Initializes a new instance of PolicyViolation. - * @param type the error type + * + * @param type the error type * @param policyErrorInfo the error details - * @throws JsonParseException if the policyErrorInfo has invalid content. - * @throws JsonMappingException if the policyErrorInfo's JSON does not match the expected schema. - * @throws IOException if an IO error occurs. + * @throws JsonParseException if the policyErrorInfo has invalid content. + * @throws JsonMappingException if the policyErrorInfo's JSON does not match the expected schema. + * @throws IOException if an IO error occurs. */ public PolicyViolation(String type, ObjectNode policyErrorInfo) throws JsonParseException, JsonMappingException, IOException { super(type, policyErrorInfo); - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + ObjectMapper objectMapper = new ObjectMapper() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); this.policyErrorInfo = objectMapper.readValue(policyErrorInfo.toString(), PolicyViolationErrorInfo.class); } - + /** * @return the policy violation error details. */ diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolationErrorInfo.java b/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolationErrorInfo.java index 6213eb3472..3a8c2292be 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolationErrorInfo.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/PolicyViolationErrorInfo.java @@ -78,7 +78,9 @@ public class PolicyViolationErrorInfo { * The policy set definition display name. */ private String policySetDefinitionDisplayName; - + + private EvaluationDetails evaluationDetails; + /** * @return the policy definition id. */ @@ -170,17 +172,48 @@ public String getPolicySetDefinitionDisplayName() { return policySetDefinitionDisplayName; } + public EvaluationDetails getEvaluationDetails() { + return evaluationDetails; + } + + @Override + public String toString() { + return "PolicyViolationErrorInfo{" + + "policyDefinitionId='" + policyDefinitionId + '\'' + + ", policySetDefinitionId='" + policySetDefinitionId + '\'' + + ", policyDefinitionReferenceId='" + policyDefinitionReferenceId + '\'' + + ", policySetDefinitionName='" + policySetDefinitionName + '\'' + + ", policyDefinitionName='" + policyDefinitionName + '\'' + + ", policyDefinitionEffect='" + policyDefinitionEffect + '\'' + + ", policyAssignmentId='" + policyAssignmentId + '\'' + + ", policyAssignmentName='" + policyAssignmentName + '\'' + + ", policyAssignmentDisplayName='" + policyAssignmentDisplayName + '\'' + + ", policyAssignmentScope='" + policyAssignmentScope + '\'' + + ", policyAssignmentParameters=" + policyAssignmentParameters + + ", policyDefinitionDisplayName='" + policyDefinitionDisplayName + '\'' + + ", policySetDefinitionDisplayName='" + policySetDefinitionDisplayName + '\'' + + ", evaluationDetails=" + evaluationDetails + + '}'; + } + /** * An instance of this class provides policy parameter value. */ public static class PolicyParameter { private JsonNode value; - + /** * @return the parameter value. */ public JsonNode getValue() { return value; } + + @Override + public String toString() { + return "PolicyParameter{" + + "value=" + value + + '}'; + } } } \ No newline at end of file diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/CloudErrorDeserializerTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/CloudErrorDeserializerTests.java index bdeaec4e8f..22387d5c80 100644 --- a/azure-client-runtime/src/test/java/com/microsoft/azure/CloudErrorDeserializerTests.java +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/CloudErrorDeserializerTests.java @@ -6,6 +6,9 @@ package com.microsoft.azure; +import java.io.IOException; +import java.util.List; + import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.azure.serializer.AzureJacksonAdapter; import com.microsoft.rest.protocol.SerializerAdapter; @@ -171,4 +174,211 @@ public void cloudErrorWitDifferentCasing() throws Exception { Assert.assertEquals("Allowed locations", policyViolation.policyErrorInfo().getPolicyDefinitionDisplayName()); Assert.assertEquals("westus", policyViolation.policyErrorInfo().getPolicyAssignmentParameters().get("listOfAllowedLocations").getValue().elements().next().asText()); } + + @Test + public void testEvaluationDetails() throws IOException { + SerializerAdapter serializerAdapter = new AzureJacksonAdapter(); + String bodyString = "{\n" + + " \"error\": {\n" + + " \"code\": \"RequestDisallowedByPolicy\",\n" + + " \"target\": \"apim-NorthWindShoes346878645\",\n" + + " \"message\": \"Resource 'apim-NorthWindShoes346878645' was disallowed by policy. (Code: RequestDisallowedByPolicy)\",\n" + + " \"additionalInfo\": [\n" + + " {\n" + + " \"type\": \"PolicyViolation\",\n" + + " \"info\": {\n" + + " \"policyDefinitionDisplayName\": \"Allowed resource types\",\n" + + " \"policySetDefinitionDisplayName\": \"webapp\",\n" + + " \"evaluationDetails\": {\n" + + " \"evaluatedExpressions\": [\n" + + " {\n" + + " \"result\": \"False\",\n" + + " \"expression\": \"type\",\n" + + " \"path\": \"type\",\n" + + " \"expressionValue\": \"Microsoft.ApiManagement/service\",\n" + + " \"targetValue\": [\n" + + " \"Microsoft.Resources/resourceGroups\",\n" + + " \"Microsoft.Storage/storageAccounts\",\n" + + " \"Microsoft.Web/sites\",\n" + + " \"Microsoft.Web/serverFarms\",\n" + + " \"Microsoft.Web/functions\",\n" + + " \"Microsoft.DocumentDB/databaseAccounts\",\n" + + " \"microsoft.insights/components\",\n" + + " \"Microsoft.KeyVault/vaults\",\n" + + " \"Microsoft.Cache/Redis\",\n" + + " \"Microsoft.ServiceBus/namespaces\",\n" + + " \"Microsoft.ServiceBus/namespaces/authorizationrules\",\n" + + " \"Microsoft.ServiceBus/namespaces/queues\",\n" + + " \"Microsoft.ServiceBus/namespaces/queues/authorizationrules\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics/authorizationrules\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics/subscriptions\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics/subscriptions/rules\",\n" + + " \"Microsoft.CognitiveServices/accounts\",\n" + + " \"Microsoft.Web/sites/slots\",\n" + + " \"Microsoft.Web/sites/slots/instances\",\n" + + " \"Microsoft.Web/sites/slots/metrics\",\n" + + " \"Microsoft.Web/sites/metrics\",\n" + + " \"Microsoft.Web/sites/instances\",\n" + + " \"Microsoft.Web/certificates\",\n" + + " \"Microsoft.Portal/dashboards\",\n" + + " \"Microsoft.ContainerRegistry/registries\",\n" + + " \"Microsoft.ContainerRegistry/registries/webhooks\",\n" + + " \"Microsoft.Web/connections\",\n" + + " \"Microsoft.Logic/workflows\",\n" + + " \"Microsoft.Web/customApis\",\n" + + " \"Microsoft.Search/searchServices\",\n" + + " \"Microsoft.Network/trafficmanagerprofiles\",\n" + + " \"Microsoft.Sql/servers\",\n" + + " \"Microsoft.Sql/servers/databases\",\n" + + " \"Microsoft.SignalRService/SignalR\"\n" + + " ],\n" + + " \"operator\": \"In\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"policyDefinitionId\": \"/providers/Microsoft.Authorization/policyDefinitions/a08ec900-254a-4555-9bf5-e42af04b5c5c\",\n" + + " \"policySetDefinitionId\": \"/providers/Microsoft.Management/managementGroups/triplecrown2/providers/Microsoft.Authorization/policySetDefinitions/f06460e7-86f9-42ad-8955-fb55bad92028\",\n" + + " \"policyDefinitionReferenceId\": \"8272972042953317437\",\n" + + " \"policySetDefinitionName\": \"f06460e7-86f9-42ad-8955-fb55bad92028\",\n" + + " \"policyDefinitionName\": \"a08ec900-254a-4555-9bf5-e42af04b5c5c\",\n" + + " \"policyDefinitionEffect\": \"deny\",\n" + + " \"policyAssignmentId\": \"/subscriptions//resourceGroups//providers/Microsoft.Authorization/policyAssignments/TripleCrownPolicy\",\n" + + " \"policyAssignmentName\": \"TripleCrownPolicy\",\n" + + " \"policyAssignmentDisplayName\": \"Sandbox Policy\",\n" + + " \"policyAssignmentScope\": \"/subscriptions//resourceGroups/\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\": \"PolicyViolation\",\n" + + " \"info\": {\n" + + " \"policyDefinitionDisplayName\": \"Allowed resource types\",\n" + + " \"policySetDefinitionDisplayName\": \"webapp\",\n" + + " \"evaluationDetails\": {\n" + + " \"evaluatedExpressions\": [\n" + + " {\n" + + " \"result\": \"False\",\n" + + " \"expression\": \"type\",\n" + + " \"path\": \"type\",\n" + + " \"expressionValue\": \"Microsoft.ApiManagement/service\",\n" + + " \"targetValue\": [\n" + + " \"Microsoft.Resources/resourceGroups\",\n" + + " \"Microsoft.Storage/storageAccounts\",\n" + + " \"Microsoft.Web/sites\",\n" + + " \"Microsoft.Web/serverFarms\",\n" + + " \"Microsoft.Web/functions\",\n" + + " \"Microsoft.DocumentDB/databaseAccounts\",\n" + + " \"microsoft.insights/components\",\n" + + " \"Microsoft.KeyVault/vaults\",\n" + + " \"Microsoft.Cache/Redis\",\n" + + " \"Microsoft.ServiceBus/namespaces\",\n" + + " \"Microsoft.ServiceBus/namespaces/authorizationrules\",\n" + + " \"Microsoft.ServiceBus/namespaces/queues\",\n" + + " \"Microsoft.ServiceBus/namespaces/queues/authorizationrules\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics/authorizationrules\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics/subscriptions\",\n" + + " \"Microsoft.ServiceBus/namespaces/topics/subscriptions/rules\",\n" + + " \"Microsoft.CognitiveServices/accounts\",\n" + + " \"Microsoft.Web/sites/slots\",\n" + + " \"Microsoft.Web/sites/slots/instances\",\n" + + " \"Microsoft.Web/sites/slots/metrics\",\n" + + " \"Microsoft.Web/sites/metrics\",\n" + + " \"Microsoft.Web/sites/instances\",\n" + + " \"Microsoft.Web/certificates\",\n" + + " \"Microsoft.Portal/dashboards\",\n" + + " \"Microsoft.ContainerRegistry/registries\",\n" + + " \"Microsoft.ContainerRegistry/registries/webhooks\",\n" + + " \"Microsoft.Web/connections\",\n" + + " \"Microsoft.Logic/workflows\",\n" + + " \"Microsoft.Web/customApis\",\n" + + " \"Microsoft.Search/searchServices\",\n" + + " \"Microsoft.Network/trafficmanagerprofiles\",\n" + + " \"Microsoft.Sql/servers\",\n" + + " \"Microsoft.Sql/servers/databases\",\n" + + " \"Microsoft.SignalRService/SignalR\"\n" + + " ],\n" + + " \"operator\": \"In\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"policyDefinitionId\": \"/providers/Microsoft.Authorization/policyDefinitions/a08ec900-254a-4555-9bf5-e42af04b5c5c\",\n" + + " \"policySetDefinitionId\": \"/providers/Microsoft.Management/managementGroups/triplecrown2/providers/Microsoft.Authorization/policySetDefinitions/f06460e7-86f9-42ad-8955-fb55bad92028\",\n" + + " \"policyDefinitionReferenceId\": \"8272972042953317437\",\n" + + " \"policySetDefinitionName\": \"f06460e7-86f9-42ad-8955-fb55bad92028\",\n" + + " \"policyDefinitionName\": \"a08ec900-254a-4555-9bf5-e42af04b5c5c\",\n" + + " \"policyDefinitionEffect\": \"deny\",\n" + + " \"policyAssignmentId\": \"/providers/Microsoft.Management/managementGroups/triplecrown2/providers/Microsoft.Authorization/policyAssignments/4a0a4629d22043e6a70a69f9\",\n" + + " \"policyAssignmentName\": \"4a0a4629d22043e6a70a69f9\",\n" + + " \"policyAssignmentDisplayName\": \"webapp\",\n" + + " \"policyAssignmentScope\": \"/providers/Microsoft.Management/managementGroups/triplecrown2\",\n" + + " \"policyAssignmentParameters\": {}\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"policyDetails\": [\n" + + " {\n" + + " \"isInitiative\": true,\n" + + " \"assignmentId\": \"/subscriptions//resourceGroups//providers/Microsoft.Authorization/policyAssignments/TripleCrownPolicy\",\n" + + " \"assignmentName\": \"Sandbox Policy\",\n" + + " \"auxDefinitionNames\": [\n" + + " \"Allowed resource types\"\n" + + " ],\n" + + " \"viewDetailsUri\": \"https://portal.azure.com#blade/Microsoft_Azure_Policy/EditAssignmentBlade/id/%2Fsubscriptions%2F%2FresourceGroups%2F%2Fproviders%2FMicrosoft.Authorization%2FpolicyAssignments%2FTripleCrownPolicy\"\n" + + " },\n" + + " {\n" + + " \"isInitiative\": true,\n" + + " \"assignmentId\": \"/providers/Microsoft.Management/managementGroups/triplecrown2/providers/Microsoft.Authorization/policyAssignments/4a0a4629d22043e6a70a69f9\",\n" + + " \"assignmentName\": \"webapp\",\n" + + " \"auxDefinitionNames\": [\n" + + " \"Allowed resource types\"\n" + + " ],\n" + + " \"viewDetailsUri\": \"https://portal.azure.com#blade/Microsoft_Azure_Policy/EditAssignmentBlade/id/%2Fproviders%2FMicrosoft.Management%2FmanagementGroups%2Ftriplecrown2%2Fproviders%2FMicrosoft.Authorization%2FpolicyAssignments%2F4a0a4629d22043e6a70a69f9\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + CloudError cloudError = serializerAdapter.deserialize(bodyString, CloudError.class); + + Assert.assertEquals("RequestDisallowedByPolicy", cloudError.code()); + Assert.assertEquals("Resource 'apim-NorthWindShoes346878645' was disallowed by policy. (Code: RequestDisallowedByPolicy)", cloudError.message()); + Assert.assertEquals("apim-NorthWindShoes346878645", cloudError.target()); + Assert.assertEquals(0, cloudError.details().size()); + Assert.assertEquals(2, cloudError.additionalInfo().size()); + Assert.assertTrue(cloudError.additionalInfo().get(0) instanceof PolicyViolation); + + PolicyViolation policyViolation = (PolicyViolation)cloudError.additionalInfo().get(0); + PolicyViolationErrorInfo policyViolationErrorInfo = policyViolation.policyErrorInfo(); + + Assert.assertEquals("Allowed resource types", policyViolationErrorInfo.getPolicyDefinitionDisplayName()); + Assert.assertEquals("webapp", policyViolationErrorInfo.getPolicySetDefinitionDisplayName()); + Assert.assertEquals("/providers/Microsoft.Authorization/policyDefinitions/a08ec900-254a-4555-9bf5-e42af04b5c5c", + policyViolationErrorInfo.getPolicyDefinitionId()); + Assert.assertEquals("/providers/Microsoft.Management/managementGroups/triplecrown2/providers/Microsoft.Authorization/policySetDefinitions/" + + "f06460e7-86f9-42ad-8955-fb55bad92028", policyViolationErrorInfo.getPolicySetDefinitionId()); + Assert.assertEquals("8272972042953317437", policyViolationErrorInfo.getPolicyDefinitionReferenceId()); + Assert.assertEquals("f06460e7-86f9-42ad-8955-fb55bad92028", policyViolationErrorInfo.getPolicySetDefinitionName()); + Assert.assertEquals("a08ec900-254a-4555-9bf5-e42af04b5c5c", policyViolationErrorInfo.getPolicyDefinitionName()); + Assert.assertEquals("deny", policyViolationErrorInfo.getPolicyDefinitionEffect()); + Assert.assertEquals("/subscriptions//resourceGroups//providers/Microsoft.Authorization/policyAssignments/" + + "TripleCrownPolicy", policyViolationErrorInfo.getPolicyAssignmentId()); + Assert.assertEquals("TripleCrownPolicy", policyViolationErrorInfo.getPolicyAssignmentName()); + Assert.assertEquals("Sandbox Policy", policyViolationErrorInfo.getPolicyAssignmentDisplayName()); + Assert.assertEquals("/subscriptions//resourceGroups/", policyViolationErrorInfo.getPolicyAssignmentScope()); + + List evaluatedExpressions = policyViolationErrorInfo.getEvaluationDetails().getEvaluatedExpressions(); + + Assert.assertEquals(1, evaluatedExpressions.size()); + + ExpressionEvaluationDetails expressionEvaluationDetails = evaluatedExpressions.get(0); + + Assert.assertEquals("False", expressionEvaluationDetails.getResult()); + Assert.assertEquals("type", expressionEvaluationDetails.getExpression()); + Assert.assertEquals("type", expressionEvaluationDetails.getPath()); + Assert.assertEquals("Microsoft.ApiManagement/service", expressionEvaluationDetails.getExpressionValue()); + Assert.assertEquals("In", expressionEvaluationDetails.getOperator()); + Assert.assertEquals(35, expressionEvaluationDetails.getTargetValue().size()); + Assert.assertTrue(expressionEvaluationDetails.getTargetValue().contains("Microsoft.Web/certificates")); + } } \ No newline at end of file