API Management – Restrict Operation Access per Product

Last week I had an interesting request from a client. They have been creating generic APIs, using a variety of different backend systems (on-prem APIs, Azure Functions, Logic Apps, cloud APIs). Those APIs were grouped by product but even within a product they would like to restrict access to only some operations, so product A would have access to a group of operations, while product B would have access to different group. Initially I thought that role based access driven by claims, but the issue was that we didn’t have control of the identity providers, or the back end services. In the end we would have to find a solution within API Management to create this ACL like style.

Initally I didn’t like the idea to create our own access controls in APIM, but faced with the other constraints, I slowly warmed up to the idea and learned a couple of things about policies along the way.

The control policy

Initial setup

To make sure that only certain operations in a policy where accessible for a given product, I’ve created a mapping document (formatted as JSON) using the following convention – “operation-name”: “All” or “Product 1|Product 2…”. For example, I’ve created a “Privileged API” example, with three operations:

  • All Access Operation
  • Low Privilege Operation
  • High Privilege Operation
  • Unregistered Operation

And with two Products:

  • Low Privileged Product
  • High Privileged Product

The mapping document in this case looks like this – stored in a name-value entry called {{privileged-api-roles}}:

{
  "all-access-operation": "All",
  "low-privilege-operation": "low-privileged-product|high-privileged-product",
  "high-privilege-operation": "high-privileged-product"
}

Notice that both the name of the operations and the products are using the some-name format. This is how the “Name” of the operations and products are defined when you are create them in API Management – I choose this option to remove the guess work when defining the mapping documents (upper x lower case, spaces, etc). Those items are represented in the context object as context.Operation.Id and context.Product.Id respectively.

To enforce the mapping, I’ve implemented the following custom policy at the API level (you can find the API definition, including the sample code below here):

<policies>
	<inbound>
		<base />
		<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
		<set-variable name="operationroles" value="{{privileged-api-roles}}" />
		<set-variable name="operationallowed" value="@{
            var responseMessage = "notallowed";
            var product = context.Product.Id;
            var operation = context.Operation.Id;
            JObject roles = JObject.Parse(context.Variables.GetValueOrDefault<string>("operationroles"));

            if (roles.ContainsKey(operation))
            {
                string evaluationRole = roles.SelectToken(operation).ToString();
                List<string> evaluationRoleList = new List<string>();
                evaluationRoleList.AddRange(evaluationRole.Split('|'));
                if (evaluationRoleList.Contains(product) || evaluationRoleList.Contains("All"))
                {
                    responseMessage = "allowed";
                }
            }

            return responseMessage;
		
		}" />
		<choose>
			<when condition="@(context.Variables.["operationallowed"] == "notallowed")">
				<return-response>
					<set-status code="403" reason="Operation not allowed for this product" />
				</return-response>
			</when>
		</choose>
	</inbound>
	<backend>
		<base />
	</backend>
	<outbound>
		<base />
	</outbound>
	<on-error>
		<base />
	</on-error>
</policies>

How the policy works

  1. a variable called operationroles is populated with the mapping document, stored in {{privileged-api-roles}}
  2. In a new operationallowed variable, some C# code execute the following actions:
    • initialize the responseMessage to “notallowed”
    • load the name of the current product from the context
    • load the name of the operation to be executed from the context
    • load the mapping document into a JObject called roles
    • look for a token containing the current operation. If exists:
      • break the value of the operation mapping into sub items (split on “|”).
      • verify if the list of items contains either “All” or the product name. If it contains:
        • set responseMessage to “allowed”
    • Return responseMessage
  3. Evaluate the responseMessage value. If it equals to not allowed
    1. Return 403 with a message that “Operation is not allowed for this product”.
    2. If allowed, continue policy evaluation.

Since this policy is implemented at the API level, it will be included in the effective policy of all operations. As long as your operation specific policy is defined after the <base/> element in the operation inbound policy, the API policy will be executed before the operation policy.

Unregistered Operations

Since the first thing the code snippet does is to find if the mapping document has a token for the operation name, adding operations to the API will make it inaccessible to ANY product, until the map is amended to include either “All” or one or more product names.

In the example defined before, the Unregistered Operation will return 403 for any product that request it.

What I learned

Initially, I thought that I would have to replicate this snippet in every operation, which would be a nightmare. But trying to understand better, I realized that the policies are bundled together at the operation level and executed once (as a group) when the operation is requested. Once I realized that, it was simply the case of applying the operation at the right level (in my case the API) and at that level I still had access to both the product name and the operation names.

One last thing I wanted to do to make sure the policy was really generic, was to programmatically access the name-value element from the code to retrieve the right map on the fly. Unfortunately, this is not how the name-value elements work. Those values are injected in the policy when it is generated for evaluation, so seems like there is no way to do what I was after.

In summary

API Management Policies are a very powerful engine that allow you to inject policies at various levels of the API composition framework it provides. Policies will help you to enhance your API definition including more security controls, caching capabilities, message transformation, among other things.

In this particular example I’ve showed you how to extend an API policy to create some kind of operation level ACL, only allowing certain products to have access to the operation.

See you next time!


Sharing is caring...

Leave a Reply

Your email address will not be published. Required fields are marked *