Custom policies have been introduced to address a heterogeneous environment of information, configurations, and actions to be executed by 3scale API Management, thereby better serving and enhancing the experience for its customers. Currently, 3scale API Management has 38 default policies, including:
3scale Auth Caching
3scaleBatcher
3scaleReferrer
AnonymousAccess
CamelService
and many more.
For most client-managed products, these policies already suffice, but in others, they need to be customized based on an existing policy or even created from scratch. So, here we will address the creation of policies from scratch.
Custom policy
By default, three main files will be created, namely an apicast-policy.json (layout of the screen to be displayed and its rules in 3scale API Management), init.lua (where the pointer will be directed to the logic of absorption and implementation of the policy), and example.lua (where the logic and behavior to handle the policy will be coded).
What are custom policies?
Custom policies are an important support tool for meeting customer needs, as existing policies are often inflexible and may not fully address what the client requires. These custom policies often originate from an existing policy used as a base, with additional rules and requirements added to meet the client’s needs.
Here, we will show how to create one from scratch, explain how they work, how they are implemented, and how they are declared.
Example of the types of files needed for the creation of each policy
Apicast-policy.json:
{ "$schema": "http://apicast.io/policy-v1/schema#manifest#", "name": "APIcast Example Policy", "summary": "This is just an example.", "description": "This policy is just an example how to write your custom policy.", "version": "0.1", "configuration": { "type": "object", "properties": { } } }
init.lua:
return require('examplo')
exemplo.lua:
local setmetatable = setmetatable local _M = require('apicast.policy').new('Example', '0.1') local mt = { __index = _M } function _M.new() return setmetatable({}, mt) end function _M:init() -- do work when nginx master process starts end function _M:init_worker() -- do work when nginx worker process is forked from master end function _M:rewrite() -- change the request before it reaches upstream ngx.req.set_header('X-CustomPolicy', 'customValue') end function _M:access() -- ability to deny the request before it is sent upstream end function _M:content() -- can create content instead of connecting to upstream end function _M:post_action() -- do something after the response was sent to the client end function _M:header_filter() -- can change response headers end function _M:body_filter() -- can read and change response body -- https://github.com/openresty/lua-nginx-module/blob/master/README.markdown#body_filter_by_lua end function _M:log() -- can do extra logging end function _M:balancer() -- use for example require('resty.balancer.round_robin').call to do load balancing end return _M
Next, we'll delve into each of these components and how we can code them.
Encode apicast-policy.json
In this section, we will describe how we will assemble the screen that will appear in 3scale API Management. With the fields and conditions necessary to meet the needs of our products. Next, we will have a sampling of how to construct the main types of fields in JSON.
Text field implementation:
{ "$schema":"http://apicast.io/policy-v1/schema#manifest#", "name":"0.1 Policy exemplo de tipos de campos", "summary":"Policy exemplo de campos.", "description":"Essa policy visa mostra a implementação em json de vários tipos de campos possíveis", "version":"0.1", "configuration": { "type": "object", "required": ["campo"], "properties": { "campo": { "description": "campo", "type": "string" } } } }
In Figure 1, an example of how the implementation of a required string field will look is shown.
Figure 1: Field of type string as required.
Combo field implementation:
{ "$schema": "http://apicast.io/policy-v1/schema#manifest#", "name": "0.1 Policy exemplo de tipos de campos", "summary": "Policy exemplo de campos.", "description": "Essa policy visa mostra a implementação em json de vários tipos de campos possíveis", "version": "0.1", "configuration": { "type": "object", "required": [ "combo" ], "properties": { "combo": { "description": "combo", "type": "string", "oneOf": [ { "enum": [ "red1" ], "title": "red hat 1" }, { "enum": [ "red2" ], "title": "red hat 2" }, { "enum": [ "red3" ], "title": "red hat 3" } ] } } } }
Figure 2 shows an example of what the implementation of a required combo box will look like.
Figure 2: Required Combo box.
Implementation of a list of added, removed, and ordered fields:
{ "$schema": "http://apicast.io/policy-v1/schema#manifest#", "name": "0.1 Policy exemplo de tipos de campos", "summary": "Policy exmplo de campos.", "description": "Essa policy visa mostra a implementação em json de varios tipos de campos possíveis", "version": "0.1", "configuration": { "description": "Lista de campos", "type": "array", "required": ["combo"], "items": { "properties": { "combo": { "description": "combo", "type": "string", "oneOf": [ { "enum": ["red1"], "title": "redhat 1" }, { "enum": ["red2"], "title": "redhat 2" }, { "enum": ["red3"], "title": "redhat 3" } ] } } } } }
Figure 3 shows an example of what the implementation of a list of fields will look like, allowing fields to be added or removed according to parameter needs.
Figure 3: Adding and removing fields in a list format.
Implement a radio button:
{ "$schema": "http://apicast.io/policy-v1/schema#manifest#", "name": "0.1 Policy exemplo de tipos de campos", "summary": "Policy exemplo de campos.", "description": "Essa policy visa mostra a implementação em json de vários tipos de campos possiveis", "version": "0.1", "configuration": { "type": "object", "required": ["radiob"], "properties": { "radiob": { "description": "Radio", "type": "string", "format":"radio", "oneOf": [ { "enum": ["red1"], "title": "redhat 1" }, { "enum": ["red2"], "title": "redhat 2" }, { "enum": ["red3"], "title": "redhat 3" } ] } } } }
Figure 4 shows an example of what the implementation of radio button selection will look like.
Figure 4: Required radio button fields.
Multiple selection implementation
To implement multiple selection, use the following code (note you must press the Ctrl key to select more than one value):
{ "$schema": "http://apicast.io/policy-v1/schema#manifest#", "name": "0.1 Policy exemplo de tipos de campos", "summary": "Policy exemplo de campos.", "description": "Essa policy visa mostra a implementação em json de vários tipos de campos possiveis", "version": "0.1", "configuration": { "type": "object", "required": ["radiob"], "properties": { "radiob": { "description": "Radio", "type": "string", "format":"radio", "oneOf": [ { "enum": ["red1"], "title": "redhat 1" }, { "enum": ["red2"], "title": "redhat 2" }, { "enum": ["red3"], "title": "redhat 3" } ] } } } }
Figure 5 shows an example of how the implementation of the required multiple choice field will appear.
This is the first file called to check what needs to be done. By default, it only calls the main file example.lua, which will be functional and contain all the rules.
init.lua:
return require('exemplo')
Encode example.lua
Here, the entire business logic necessary for the functioning of the policy will be created. However, before starting with the logic, we need to understand the main methods, the order in which they are called, and at what moment they are called.
A policy informs APIcast what it should do in each of the nginx phases: init, init_worker, ssl_certificate, rewrite, access, content, balancer, header_filter, body_filter, post_action, log, and metrics.
Policies can share data between them. They do this through what we call context. Policies can read and modify this context in all phases.
For example, the way policy chains work is as follows: suppose we have a Policy A that describes what to do in the rewrite and header_filter phases, and a Policy B that describes what to execute in the access and header_filter phases. Suppose also that when describing the chain, we indicate that Policy A should be executed before Policy B.
When APIcast receives an HTTP request, it will check the policy chain and execute the tasks described in the following table:
Stage
APIcast Task
rewrite
Executes the function provided by Policy A for this phase.
access
Executes the function provided by Policy B for this phase.
content
None. Neither Policy A nor Policy B describes what to do.
balancer
None. Neither Policy A nor Policy B describes what to do.
header_filter
Executes the function provided by Policy A for this phase, and then the function provided by Policy B for this phase. The policy chains define an order, and we specified that Policy A comes before Policy B.
body_filter
None. Neither Policy A nor Policy B describes what to do.
post_action
None. Neither Policy A nor Policy B describes what to do.
log
None. Neither Policy A nor Policy B describes what to do.
Note that there is no description of what APIcast does in the init and init_worker phases. The reason is that these two are not executed in all requests. init is executed when APIcast is initialized, and init_worker is executed when each of its workers is started.
Another phase that is not executed for all requests is ssl_certificate because it is called only when APIcast terminates the HTTPS connection.
The order in which policy actions are applied depends on two factors:
The position of the policy within the policy chain.
The phase in which the policies operate.
This means that sometimes the result of the execution of policies can be affected by other policies located later in the policy chain.
For example, suppose we combine the APIcast policy (the default one) with the URL rewrite policy (which modifies the URL path based on some defined rules). If the URL rewrite policy appears before the APIcast policy, the APIcast mapping rules will be applied to the rewritten path. However, if the URL policy appears after the APIcast policy, the mapping rules will be applied to the original path.
To illustrate this, below we have a coding example that shows how these phases or stages are executed.
Execution stages of a policy:
local policy = require('apicast.policy') local _M = policy.new('Exemplo de criação Campos') local new = _M.new function _M.new(config) local self = new(config) ngx.log(ngx.INFO, 'estagio new -----------:') return self end function _M:init() ngx.log(ngx.INFO, 'estagio init -------:') end function _M:init_worker() ngx.log(ngx.INFO, 'estagio init_worker -----:') end function _M:rewrite() ngx.log(ngx.INFO, 'estagio rewrite -------:') end function _M:access() ngx.log(ngx.INFO, 'estagio access ---------:') end function _M:content() ngx.log(ngx.INFO, 'estagio content ---------------------:') end function _M:post_action() ngx.log(ngx.INFO, 'estagio post_action --------------:') end function _M:header_filter() ngx.log(ngx.INFO, 'estagio header_filter -----------:' ) end function _M:body_filter() ngx.log(ngx.INFO, 'estagio body_filter -------------:' ) end function _M:log() ngx.log(ngx.INFO, 'estagio log --------:' ) end function _M:balancer() ngx.log(ngx.INFO, 'estagio balancer ---------------------:' ) end return _M
In Figure 6, representing a log extracted from the policy in operation, we see the order of method processing and which methods are used only in the API Cast and which are used in each product call.
Figure 6: Presentation of the processing order of the methods.
Implementation to fetch the declared fields in the policy:
local policy = require('apicast.policy') local _M = policy.new('Exemplo de criação Campos') local new = _M.new function _M.new(config) local self = new(config) self.campoString = config.campoString self.campoint= config.campoint self.radiob= config.radiob self.combo= config.combo self.multiEnum= config.multiEnum self.variosCampos = config.variosCampos ngx.log(ngx.INFO, 'estagio new -------------:') -- campos simples ---- ngx.log(ngx.INFO, 'estagio new - campoString --------- :', self.campoString ) ngx.log(ngx.INFO, 'estagio new - campoInt --------- :', self.campoint ) ngx.log(ngx.INFO, 'estagio new - camporadio --------- :', self.radiob ) ngx.log(ngx.INFO, 'estagio new - combo --------- :', self.combo ) -- campos multiplos ---- -- pegar o tamanho do array ---- quantSelectMul = #self.multiEnum ngx.log(ngx.INFO, 'estagio new - quantidade Selecionada --------- :', quantSelectMul ) -- por default a codificação lua começa no indice 1 --- for i = 1, quantSelectMul do ngx.log(ngx.INFO, 'estagio new - seleção de varios --------- :', self.multiEnum[i] ) end quantCamposMul = #self.variosCampos ngx.log(ngx.INFO, 'estagio new - quantidade campos --------- :', quantCamposMul ) for j = 1, quantCamposMul do ngx.log(ngx.INFO, 'estagio new - campos --------- :', self.variosCampos[j].campo ) end -- essa construção e valores ficaram guardados para utilização na variavel local -- self para ser chamado nas outras etapas return self end function _M:init() ngx.log(ngx.INFO, 'estagio init --------------:' ) end function _M:init_worker() ngx.log(ngx.INFO, 'estagio init_worker --------:' ) end function _M:rewrite() ngx.log(ngx.INFO, 'estagio rewrite ------------:' ) ngx.log(ngx.INFO, 'estagio rewrite - camporadio --------- :', self.radiob ) end function _M:access() ngx.log(ngx.INFO, 'estagio access --------------:' ) end function _M:content() ngx.log(ngx.INFO, 'estagio content -------------:' ) end function _M:post_action() ngx.log(ngx.INFO, 'estagio post_action -----------:' ) end function _M:header_filter() ngx.log(ngx.INFO, 'estagio header_filter ---------:' ) end function _M:body_filter() ngx.log(ngx.INFO, 'estagio body_filter -----------:' ) end function _M:log() ngx.log(ngx.INFO, 'estagio log -------------------:' ) end function _M:balancer() ngx.log(ngx.INFO, 'estagio balancer --------------:' ) end return _M
Figure 7 shows the log output with the values captured by the implementation.
Figure 7: Demonstration in the log of the values captured in the implemented policy.
Declare the custom policy
With OpenShift and 3scale API Management installed and configured, we can move onto declaring the custom policy.
Configure the policy in 3scale API Management
Select the 3scale API Management project within OpenShift. In Secrets, click Create->Key/value (Figure 8).
Figure 8: Secrets creation screen.
Thus, we give a name to our secret and create 3 key/value pairs with the files created: init.lua, exemplo.lua, and apicast-policy.json as shown in Figure 9.
Figure 9: Creation of secrets.
After that, we need to configure the YAML of 3scale-apim to find this secret to appear as a policy in 3scale API Management. It will be declared in productionSpec and stagingSpec as shown in Figure 10.
Figure 10: YAML of the policy declaration.
After the 3scale API Management pod is restarted, the policy will appear for us to add it to our product according to your needs. For products created within OpenShift that need this secret, the same process must be followed. Create the secret and then declare it in your APICast as shown in Figure 11.
Figure 11: Policy declaration in the product.
With this, we can start creating our own policies and implementing the necessary rules to meet our demands.