Get Base URL and that with version identifier via Azure APIM Policy - azure

Is it possible to, within policy, get the base url and that with version hightlighted below:
A url is needed like below:
#(base url with version identifier)
Above is used in find-and-replace element below:
<policies>
<inbound>
<base />
<set-backend-service base-url="https://my.com/oidc" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<find-and-replace from="https://thirdparty/certs"
to="#(base url with version identifier)/certs" />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Update
How can I get the base url too? The reason is that sometimes, the versioned url is not specified, in this case, only the base url is used to repalce.
https://learn.microsoft.com/en-us/azure/api-management/api-management-policy-expressions#ref-iurl

You can get the base url with version using the below code snippet
#{
var methodRoute = context.Request.Url.ToString().Replace(context.Api.ServiceUrl.ToString(),"");
var frontEndServiceUrl = context.Request.OriginalUrl.ToString().Replace(methodRoute,"");
}

Related

azure apim import policy - Too many characters in character literal

I'm trying to import a policy to azure api management using a bicep template as follows:
resource allOpsPolicy 'Microsoft.ApiManagement/service/apis/policies#2021-12-01-preview' = {
name: 'policy'
parent: apimapi_v1
properties: {
value: loadTextContent('all-ops-policy.xml')
format: 'xml'
}
}
The content of the all-ops-policy.xml file is as follows:
<policies>
<inbound>
<base />
<choose>
<when condition="#(context.Request.OriginalUrl.ToString().EndsWith("Public") == false)">
<rewrite-uri template="#(String.Concat(context.Request.OriginalUrl.ToString(),"/", "MapServer"))" copy-unmatched-params="false" />
</when>
<otherwise />
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
When run I get the following error:
One or more fields contain incorrect values: (Code: ValidationError)
'Public' is an unexpected token. Expecting white space.
Can anyone see where I'm going wrong?
The problem was the value of the format property of the allOpsPolicy resource.
I changed from
format: 'xml'
to
format: 'rawxml'
then it worked!
Thanks to Nisha for solving this one
Please replace the single quotes ` with double quotes ".
context.Request.OriginalUrl.ToString().EndsWith('Public') == false)
context.Request.OriginalUrl.ToString().EndsWith("Public") == false
It seems there's also one single quote too much:
String.Concat(context.Request.OriginalUrl.ToString(),'/'', 'MapServer')
String.Concat(context.Request.OriginalUrl.ToString(),"/", "MapServer")
Complete policy:
<policies>
<inbound>
<base />
<choose>
<when condition="#(context.Request.OriginalUrl.ToString().EndsWith("Public") == false)">
<rewrite-uri template="#(String.Concat(context.Request.OriginalUrl.ToString(),"/", "MapServer"))" copy-unmatched-params="false" />
</when>
<otherwise />
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Issue is not reproducable:

Azure API Management Policy- "rewrite-url" policy

I am trying to create a policy, that will pull the insurer ID out of the input body and put it in the URL as shown in the picture below.
Trying to clean up request body and reconstruct URL so the request can successfuly post to our approve endpoint
Expected Result:
https://apimanagement.test.com/consto-123/api/relationship/approve/{id}
Request body:
{
"insurer_name": "Tony",
"insurer_id": "12345",
"comments": "This is test"
}
You have to extract the insurer_id from the request-body and store it as a variable:
<set-variable name="insurerId" value="#{
var body = context.Request.Body.As<JObject>(true);
return body["insurer_id"].Value<string>();
}" />
Afterwards you can use rewrite-uri with this variable and forward the request to the blackened.
The complete policy:
<policies>
<inbound>
<base />
<set-variable name="insurerId" value="#{
var body = context.Request.Body.As<JObject>(true);
return body["insurer_id"].Value<string>();
}" />
<rewrite-uri template="#("/api/relationship/approve/" + context.Variables.GetValueOrDefault<string>("insurerId"))" copy-unmatched-params="false" />
<set-backend-service base-url="https://rfqapiservicey27itmeb4cf7q.azure-api.net" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>

How to share xml_content in Terraform API Management resource

We are creating our API management instance and associated endpoints through Terraform. All our API endpoints (close to a hundred) share the same policy logic for routing the request to an Azure function.
An example policy is like so -
resource "azurerm_api_management_api_operation_policy"
"api_put_policy" {
api_name = azurerm_api_management_api.my_api.name
resource_group_name = azurerm_resource_group.main.name
api_management_name = azurerm_api_management.my_api.name
operation_id = azurerm_api_management_api_operation.my_api.operation_id
xml_content = <<XML
<policies>
<inbound>
<base />
<choose>
<when condition="#(context.Request.Headers.GetValueOrDefault("Key") == "password")">
<set-backend-service base-url="${data.azurerm_function_app.MyFunctionApp.default_hostname}" />
</when>
<when condition="#(context.Request.Headers.GetValueOrDefault("Key") != null)">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-body>An incorrect Key header has been passed in the request</set-body>
</return-response>
</when>
<otherwise>
<set-backend-service base-url="${other-route-variable}" />
</otherwise>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
So we have the same XML_content being used on every API endpoint, only the variables get set differently depending what function app is going to be routed to.
Is there a way this xml content could be moved into a file where parameters can be passed through to then generate the XML for every API policy so we have the logic stored in only one place?
I have looked at a variety of uses of the file() function but can't see anything that could be done to achieve what I need here.
Thanks
Yes, you can use the templatefile function for that [1]. The templatefile function works in the following way:
templatefile(path, vars)
Where the path represents the file location and the vars are a map of variables that will be used to replace the placeholders in the file itself. I will give an example based on the XML file you have. You would first create the template file inside of the same directory probably (e.g., xml_content.tpl):
<policies>
<inbound>
<base />
<choose>
<when condition="#(context.Request.Headers.GetValueOrDefault("Key") == ${password})">
<set-backend-service base-url="${hostname_url}" />
</when>
<when condition="#(context.Request.Headers.GetValueOrDefault("Key") != null)">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-body>An incorrect Key header has been passed in the request</set-body>
</return-response>
</when>
<otherwise>
<set-backend-service base-url="${other-route-variable}" />
</otherwise>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
Notice that I removed the password value and the data source output in url. Those will now expect variables with names password and hostname_url to be provided when using the templatefile function:
resource "azurerm_api_management_api_operation_policy" "api_put_policy" {
api_name = azurerm_api_management_api.my_api.name
resource_group_name = azurerm_resource_group.main.name
api_management_name = azurerm_api_management.my_api.name
operation_id = azurerm_api_management_api_operation.my_api.operation_id
xml_content = templatefile("${path.root}/xml_content.tpl",
password = var.password
hostname_url = data.azurerm_function_app.MyFunctionApp.default_hostname
)
}
Whenever this is called, it will look for the placeholder values and replace them. Two additional things to note:
With the current setup, the "${other-route-variable}" would be required to be provided in the templatefile function call, otherwise it would fail.
The path.root option is built-in in Terraform [2].
In theory, if you were to create a module from this to make it more portable, then you would just have to change the path to the file so it can be provided through a variable probably.
[1] https://www.terraform.io/language/functions/templatefile
[2] https://www.terraform.io/language/expressions/references#filesystem-and-workspace-info

Azure API management not sending retry when function app disabled

I've configured an API Management policy to retry to a second region if the first encounters an error.
However, when I disable the primary function app, based in Sydney, there are no retries recorded in Application insights - only the original failing request. There are no function calls recorded in the Melbourne function app.
I've tried a bunch of different configurations. Both region function apps are running fine.
I've got the copied the code below, is there something that I'm missing or not understanding?
Thanks in advance
API Policy
<policies>
<inbound>
<base />
<choose>
<when condition="#(context.Variables.GetValueOrDefault<int>('RetryCounter', 0) == 0)">
<set-backend-service base-url="{{syndey-function-app}}" />
</when>
<otherwise>
<set-backend-service base-url="{{melbourne-function-app}}" />
</otherwise>
</choose>
</inbound>
<backend>
<retry count="1" interval="0" first-fast-retry="true" condition="#(
context.Response.StatusCode > 400
)">
<set-variable name="RetryCounter" value="#(context.Variables.GetValueOrDefault<int>('RetryCounter', 0) + 1)" />
<forward-request buffer-request-body="true" timeout="15"/>
</retry>
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
403 Error Response
<h1 id="unavailable">Error 403 - This web app is stopped.</h1>
<p id="tryAgain">
The web app you have attempted to reach is currently stopped and does not
accept any requests. Please try to reload the page or visit it again soon.
</p>
<p id="toAdmin">
If you are the web app administrator, please find the common 403 error
scenarios and resolution <a href="https://go.microsoft.com/fwlink/?linkid=2095007"
target="_blank">here</a>. For further troubleshooting tools and recommendations,
please visit Azure Portal.
</p>
I had to replace the single quotes to double quotes because of an error on save:
context.Variables.GetValueOrDefault<int>('RetryCounter', 0) => context.Variables.GetValueOrDefault<int>("RetryCounter", 0)
For testing I created two mocked services:
Returns status code 200:
https://rfqapiservicey27itmeb4cf7q.azure-api.net/echo/200/test
Returns status code 403:
https://rfqapiservicey27itmeb4cf7q.azure-api.net/echo/403/test
The retry section in the backend section will never return to the inbound section. So the choose condition was only hit at the very beginning.
Therefore, the choose condition has to be moved to the backend section.
<policies>
<inbound>
<base />
<set-backend-service base-url="https://rfqapiservicey27itmeb4cf7q.azure-api.net/echo/403" />
<!--
<set-backend-service base-url="{{syndey-function-app}}" />
-->
</inbound>
<backend>
<retry count="1" interval="0" first-fast-retry="true" condition="#(
context.Response.StatusCode > 400
)">
<set-variable name="RetryCounter" value="#(context.Variables.GetValueOrDefault<int>("RetryCounter", 0) + 1)" />
<choose>
<when condition="#(context.Variables.GetValueOrDefault<int>("RetryCounter", 0) > 1)">
<set-backend-service base-url="https://rfqapiservicey27itmeb4cf7q.azure-api.net/echo/200" />
<!--
<set-backend-service base-url="{{melbourne-function-app}}" />
-->
</when>
</choose>
<forward-request buffer-request-body="true" timeout="15" />
</retry>
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
My sample operation executes as expected:
https://rfqapiservicey27itmeb4cf7q.azure-api.net/sample/test
The first hit in the backend requests the 403 resource:
This request is failing, and the retry policy sets the 200 in the backend section:
This second request is successful when will return 200 and leave the backend section:

Why can you not access the Content-Type in a Azure API Management policy?

As part of the inbound request I want to assess which format the data has been posted in as so I can convert from one to another. Using I am trying to get the content type;
string contentType = context.Request.Headers.GetValueOrDefault("Content-Type", "text/plain");
I was getting the default value (text/plain) no matter what I set the Content-Type in the request. I recorded the headers to see what was being set and it appears that the inbound policy does get the Content-Type header. Weird that you can set but not retrieve it.
How do I determine what format I have been sent?
It's working with your provided code:
Content-Type: application\json
The complete policy:
<policies>
<inbound>
<base />
<set-body template="none">#{
string contentType = context.Request.Headers.GetValueOrDefault("Content-Type", "text/plain");
return contentType;
}</set-body>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>

Resources