Generics in APIM policy breaks Terraform - azure

The following APIM policy cannot be deployed via Terraform:
<policies>
<inbound>
<send-request mode="new" response-variable-name="xxx" timeout="20" ignore-error="true">
<set-url>https://xxx/api/v1/xxx?code={{xxx}}</set-url>
<set-method>GET</set-method>
</send-request>
<return-response>
<set-status code="302" reason="Redirect" />
<set-header name="Location" exists-action="override">
<value>#{
var response = context;
IResponse variable = context.Variables["xxx"] as IResponse;
var content = variable.Body;
JObject obj = content.As<JObject>(); <------------ < and > characters
JProperty urlprop = obj.Property("xxx");
return "" + urlprop.Value;
}
</value>
</set-header>
</return-response>
</inbound>
...
</policies>
With the following error: The 'JObject' start tag on line 15 position 26 does not match the end tag of 'value'. Line 18, position 4.
This is due to the content.As<JObject>.
The policy is defined as in the Terraform documentation.
Same error when using xml_content = "${file("xxx")}".
How to proceed, besides finding a workaround to not use content.As<JObject>?

The multilines statement (#{ }) can contain XML unfriendly characters, but not via the Terraform XML reader.
Use < and > if the XML policy is provisioning by Terraform.

Related

Unable to set body & return response in APIM policies, getting 500 error

I am using this code for azure APIM policies
<set-variable name="newRequest" value="#(context.Request.Body?.As<JObject>(preserveContent: true))" />
<set-variable name="insured-id" value="#(context.Request.MatchedParameters["id"].Last())" />
<send-request mode="new" timeout="20" response-variable-name="id" ignore-error="false">
<set-url>#($"https://api.dev.com/external/workRequest/get")</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="Authorization" exists-action="override">
<value>#(context.Request.Headers.GetValueOrDefault("Authorization","scheme param"))</value>
</set-header>
<set-body>{"insuredId": #($"{(string)context.Variables["insured-id"]}")}</set-body>
</send-request>
<choose>
<when condition="#((int)((IResponse)context.Variables["id"]).Body.As<JObject>(preserveContent: true)["workRequests"]["entityStatus"]== 1)">
<return-response response-variable-name="id">
<set-status code="400" reason="VOID" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value></set-header>
<set-body>{"statusCode": 400,
"message": "The insured cannot be voided as it is currently attached with one or more active workrequest"}</set-body>
</return-response>
</when>
<otherwise />
</choose>
I am taking an insuredId from template parameter of the API operation where I am implementing the APIM policies & using it in the set-body, this will list all the workrequests for that insuredId.
The payload for the POST is something like
{"insuredId": template-parameter}
When returning a response getting 500 error. How to resolve this. The condition which is there is okay. I am suspecting error in set body.
Also how to check whether a particular string like "entityStatus": 1 is there in the response of api, because this https://api.dev.com/external/workRequest/get url will give a list of workrequest records in array form
The request to https://api.dev.com/external/workRequest/get is failing:
"message": "POST request to 'https://api.dev.com/external/workRequest/get' has been sent, result stored in 'id' variable.",
"statusCode": "MethodNotAllowed",
"statusReason": "Method Not Allowed",
The response of failed request is stored in the variable id.
But this response not return any json document in the body.
Therefore, the following expression is failing and an error 500 is returned:
"(int)((IResponse)context.Variables[\"id\"]).Body.As<JObject>(preserveContent: true)[\"workRequests\"][\"entityStatus\"]== 1",
The message body is not a valid JSON. Unexpected character encountered while parsing value: <. Path '', line 0, position 0.\r\n at Newtonsoft.Json.JsonTextReader.ParseValue()\r\n at Microsoft.WindowsAzure.ApiManagement.Proxy.Runtime.Configuration.Models.SyncOnlyDepthTrackingJsonTextReader.Read()\r\n at Newtonsoft.Json.Linq.JObject.Load(JsonReader reader, JsonLoadSettings settings)\r\n at Gateway.Pipeline.IO.JObjectFormatter.Format(JsonTextReader jsonTextReader)\r\n at Gateway.Pipeline.IO.JsonConverter.Convert[T](Stream stream, Encoding encoding, ILog log, Object conversionSettings)\r\n at Microsoft.WindowsAzure.ApiManagement.Proxy.Gateway.MessageBody.As[T](Boolean preserveContent)
Please check the supported http-methods of https://api.dev.com/external/workRequest/get.
Maybe your code has to changed to GET:
<send-request mode="new" timeout="20" response-variable-name="id" ignore-error="false">
<set-url>#($"https://api.dev.com/external/workRequest/get")</set-url>
<set-method>GET</set-method>
Trace of request:
UPDATE for question from comments:
thanks, I resolved the issue. The when condition I was using was syntactically wrong as I had a response in the form of array list { "workRequests": [{ "id": 154, "workRequestNumber": "W000154", "insured": {"id": 268,"uri": null, "entityStatus": 1,"workRequestStatus": 0,"entityStatus": 1 },....
It's better to create a new Stackoverflow Question instead of doing it in comments.
Maybe that's what you asking for:
Request body:
{
"workRequests": [
{
"id": 154,
"workRequestNumber": "W000154",
"insured": {
"id": 268,
"uri": null,
"entityStatus": 1,
"workRequestStatus": 0
}
},
{
"id": 154,
"workRequestNumber": "W000154",
"insured": {
"id": 268,
"uri": null,
"entityStatus": 0,
"workRequestStatus": 0
}
}
]
}
Response:
A variable is assigned with a true value of any entityStatus equals 1.
The choose policy returns a 400 with entityStatus equals true .
Policy:
<inbound>
<base />
<set-variable name="isEntityStatusEqualsOne" value="#{
var body = (JObject)context.Request.Body.As<JObject>(true);
JArray workrequests = body["workRequests"] as JArray;
for(int i = 0; i < workrequests.Count; i++)
{
JObject workrequest = workrequests[i] as JObject;
var entityStatus = ((JValue)workrequest["insured"]["entityStatus"]).Value<int>();
if(entityStatus == 1)
{
return true;
}
}
return false;
}" />
<choose>
<when condition="#(context.Variables.GetValueOrDefault<bool>("isEntityStatusEqualsOne") == true)">
<return-response response-variable-name="id">
<set-status code="400" reason="VOID" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{"statusCode": 400,
"message": "The insured cannot be voided as it is currently attached with one or more active workrequest"}</set-body>
</return-response>
</when>
<otherwise>
<return-response response-variable-name="id">
<set-status code="200" reason="OK" />
</return-response>
</otherwise>
</choose>
</inbound>

Change response body in azure api management service

I am testing out a GET request with APIm using json placeholder api and cannot replace my response body. Any help is appreciated!
JSON response:
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
Here is my policy:
<policies>
<inbound>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<choose>
<when condition="#(context.Response.StatusCode == 200)">
<set-body>#{
var response = context.Response.Body.As<JObject>();
response["id"] = "Hello World!";
return response.ToString();
}
</set-body>
</when>
</choose>
</outbound>
<on-error>
<base />
</on-error>
</policies>
I end up with this error:
set-body (7.739 ms) {
"messages": [
{
"message": "Expression evaluation failed.",
"expression": "\n var response = context.Response.Body.As();\n response["id"] = 2;\n
return response.ToString();\n ",
"details": "The message body is not a valid JSON. Unexpected character encountered while parsing value: �. Path '', line
0, position 0.\r\n at
Newtonsoft.Json.JsonTextReader.ParseValue()\r\n at
Newtonsoft.Json.Linq.JObject.Load(JsonReader reader, JsonLoadSettings
settings)\r\n at
Microsoft.WindowsAzure.ApiManagement.Proxy.Gateway.MessageBody.AsJObject(Stream
stream, Encoding encoding, JsonSerializerSettings settings)\r\n at
Microsoft.WindowsAzure.ApiManagement.Proxy.Gateway.MessageBody.As[T](Boolean
preserveContent)"
},
"Expression evaluation failed. The message body is not a valid JSON. Unexpected character encountered while parsing value: �. Path
'', line 0, position 0.\r\n at
Newtonsoft.Json.JsonTextReader.ParseValue()\r\n at
Newtonsoft.Json.Linq.JObject.Load(JsonReader reader, JsonLoadSettings
settings)\r\n at
Microsoft.WindowsAzure.ApiManagement.Proxy.Gateway.MessageBody.AsJObject(Stream
stream, Encoding encoding, JsonSerializerSettings settings)\r\n at
Microsoft.WindowsAzure.ApiManagement.Proxy.Gateway.MessageBody.As[T](Boolean
preserveContent)",
"Unexpected character encountered while parsing value: �. Path '', line 0, position 0."
] }
update 1:
Executing this policy and shows that the body is null. Is there a way to wait for the response body? When I debug it within vscode and step through it, I'm getting the response successfully. Here's my new outbound policy:
<policies>
<inbound>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<return-response>
<set-header name="Content-Type">
<value>application/json</value>
</set-header>
<set-header name="Accept">
<value>application/json</value>
</set-header>
<set-body>#{
JObject body = null;
try {
body = context.Response.Body.As<JObject>(preserveContent: true);
return "{\"id\":"+ #body["type"] + "}";
} catch (Exception e) {
return JsonConvert.SerializeObject(context.Response.Body);
}
}
</set-body>
</return-response>
</outbound>
<on-error>
<base />
</on-error>
</policies>
Update 2
Added a trace:
<trace source="test">
<message>#{
return context.Response.Body.As<string>(preserveContent: true);
}</message>
</trace>
result of the trace:
test (0.006 ms)
"��\u0002\u0000 ���%\u001d�WV\rm%\n:\u0018�˫�y�ȁ[�P۝߁��>�\u0000�w9\u0015s\b�èS7���˻\u0013�\u0005늇>qs*��\f�\u0001"
Setting the header to Accept-Encoding: * gave me a successful response. I thought Content-Type: application/json; charset=utf-8 was enough for encoding. I need to do a deeper dive Accept-Encoding but when I saw the error in the trace, it looked like an encoding issue and that's how I arrived with my solution.

Azure Api Management - cors policy. Cannot provide <allowed-origins> from named values dynamically

Objective:
Dynamically pass cors origins to the cors policy from named values.
Example:
Suppose I have the following named values:
name: cors-origins
value: https://domain1.com;https://domain2.com
and the CORS policy which intends to make use of this named value cors-origins:
<policies>
<inbound>
<cors>
<allowed-origins>
// Here I need to use {{ cors-origins }}
// The expected output is:
// <origin>https://domain1.com</origin>
// <origin>https://domain2.com</origin>
</allowed-origins>
<allowed-methods>
<method>GET</method>
<method>POST</method>
</allowed-methods>
</cors>
</inbound>
<backend>
<forward-request />
</backend>
<outbound />
<on-error />
</policies>
What I tried so far:
Dynamically creating the <allowed-origins> section:
Snippet:
...
<cors>
#{
var origins = "{{cors-origins}}".Split(';');
return new XElement("allowed-origins", origins.Select(domain => new XElement("origin",domain)));
// outputs:
// <allowed-origins>
// <origin>https://domain1.com</origin>
// <origin>https://domain2.com</origin>
// </allowed-origins>
}
...
</cors>
Errors with: The element 'cors' cannot contain text. List of possible elements expected: 'allowed-origins, allowed-headers, expose-headers, allowed-methods'
Dynamically creating only the <origin> elements.
Question: Is there a way to achieve the intended goal?
Not possible to dynamically add tags in APIM policies. What you can do is to read the request origin from the request header and check if it exists in one of the list of allowed origins.
Reference:
How to make Azure API Management (API Gateway) CORS policy dynamic?
For your case it should be something like:
<fragment>
<cors allow-credentials="true">
<allowed-origins>
<origin>#{
var origins = "{{cors-origins}}".Split(';');
string origin = context.Request.Headers.GetValueOrDefault("Origin", "");
bool isAllowedOrigin = Array.IndexOf(origins, origin) > -1;
return isAllowedOrigin ? origin : "";
}</origin>
</allowed-origins>
<allowed-methods preflight-result-max-age="300">
<method>*</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
<expose-headers>
<header>*</header>
</expose-headers>
</cors>

How configure Azure API Management for CORS

I have create an Azure API Management Service and connected my APIs. I added CORS policies to them.
I checked the Calculate effective policy and the result is this policy
<policies>
<inbound>
<!-- base: Begin Product scope -->
<!-- base: Begin Global scope -->
<cors allow-credentials="true">
<allowed-origins>
<origin>https://developer.mydomain.com</origin>
</allowed-origins>
<allowed-methods preflight-result-max-age="300">
<method>*</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
<expose-headers>
<header>*</header>
</expose-headers>
</cors>
<!-- base: End Global scope -->
<!-- base: End Product scope -->
<cors>
<allowed-origins>
<origin>*</origin>
</allowed-origins>
<allowed-methods>
<method>GET</method>
<method>POST</method>
</allowed-methods>
</cors>
</inbound>
<backend>
<!-- base: Begin Product scope -->
<!-- base: Begin Global scope -->
<forward-request />
<!-- base: End Global scope -->
<!-- base: End Product scope -->
</backend>
<outbound />
<on-error />
</policies>
If I call the API with C#, it is working. Then, I created a simple JavaScript to call the API but there is no way to avoid CORS. I tried different JavaScript
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<script>
var url = '';
var headerKey = 'subscription-Key';
var headerValue = 'e1e21';
$.ajax({
url: url,
beforeSend: function(xhrObj){
// Request headers
xhrObj.setRequestHeader("Origin","https://www.puresourcecode.com/");
xhrObj.setRequestHeader("Access-Control-Allow-Origin","https://www.puresourcecode.com/");
xhrObj.setRequestHeader("Access-Control-Request-Method","GET");
xhrObj.setRequestHeader("Access-Control-Allow-Credentials","true");
xhrObj.setRequestHeader("Access-Control-Request-Headers","X-Custom-Header");
xhrObj.setRequestHeader("Access-Control-Allow-Headers","Origin, Content-Type, Accept, Authorization, X-Request-With");
xhrObj.setRequestHeader(headerKey,headerValue);
},
type: "GET",
contentType: "application/json; charset=utf-8",
})
.done(function(data) {
alert("success");
})
.fail(function() {
alert("error");
});
// jQuery preflight request
$.ajax({
type: "GET",
headers: {headerKey: headerValue},
url: url
}).done(function (data) {
console.log(data);
});
// XMLHttpRequest preflight request
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.setRequestHeader(headerKey, headerValue);
xhr.onload = function () {
console.log(xhr.responseText);
};
xhr.send();
// Fetch preflight request
var myHeaders = new Headers();
myHeaders.append(headerKey, headerValue);
fetch(url, {
headers: myHeaders
}).then(function (response) {
return response.json();
}).then(function (json) {
console.log(json);
});
</script>
</body>
</html>
All of them are failing because of CORS. I tried to add preflight-result-max-age="300" and also specified the allowed-headers like the one I use to pass the subscription key without success. I also tried to copy this file in my server with the same error.
As origin I set * because I thought in this way every request for every URL is accepted but obviously not.
What is the correct settings to apply in the Azure API Management to avoid CORS?
I think to issue happens when you enable the Developer portal. Then, in the configuration for the portal, you enable CORS. At this point, Azure automatically adds a global policy to the API Management Service but you don't know that.
<policies>
<inbound>
<!-- base: Begin Global scope -->
<cors allow-credentials="true">
<allowed-origins>
<origin>https://developer.mydomain.com</origin>
</allowed-origins>
<allowed-methods preflight-result-max-age="300">
<method>*</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
<expose-headers>
<header>*</header>
</expose-headers>
</cors>
<!-- base: End Global scope -->
</inbound>
</policies>
Happily, you configure CORS in your API and when you try to call them from JavaScript, you are facing the CORS error.
If you open the CORS configuration in the Azure portal you can see something like that:
<policies>
<inbound>
<base />
<cors>
<allowed-origins>
<origin>*</origin>
</allowed-origins>
<allowed-methods preflight-result-max-age="300">
<method>GET</method>
<method>POST</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
<header />
</allowed-headers>
<expose-headers>
<header>*</header>
</expose-headers>
</cors>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
My understanding is the portal combine the base configuration with yours. So, like in Active Directory the more restricted policy is applying. That means in my example, only the Developer Portal can call the APIs without CORS issue.
Solution
Delete <base /> under <inbound> and save.
Yes, updating the CORS in the global setting solved our problem. We spent a lot of time troubleshooting by overriding the policy for a specific service to identify the root cause. It turned out to be that we have to apply the CORS policy at the global level.API Management Service -> APIS -> All APIS -> Inbound processing settings
UI Screenshot
I was running into the same issue on every API Management I set up. I keep forgetting what I did to fix it, so this is also a reminder to my future self.
Add a CORS policy,
Allowed headers *
Enable Allow credentials
Include Slash in URL

Azure API Management Rate Limit By Json Body

hello i am trying applying rate limiting on azure api management by json body value i have rule like that
<rate-limit-by-key calls="6" renewal-period="180" counter-key="#(context.Request.Body.As<JObject>()["phoneNumber"].ToString())" increment-condition="#(context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)" />
but rate limiting not working.
My test was succesful.
Request-Body:
{
"phoneNumber": "+1234"
}
Policy:
<inbound>
<base />
<rate-limit-by-key calls="6" renewal-period="180" counter-key="#(context.Request.Body.As<JObject>()["phoneNumber"].ToString())" increment-condition="#(context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)" />
<return-response>
<set-status code="200" reason="OK" />
<set-body />
</return-response>
</inbound>
return-response is just used for testing.
Response-Body after a few tries:
{
"statusCode": 429,
"message": "Rate limit is exceeded. Try again in 174 seconds."
}
How is this different from your implementation?
Do you have a different Request-Body?

Resources