Skip to content

Commit

Permalink
Merge pull request #1465 from tkan145/THREESCALE-10973-fapi-baseline
Browse files Browse the repository at this point in the history
THREESCALE-10973 - Support Financial-grade API (FAPI) - Baseline profile
  • Loading branch information
tkan145 authored Jun 14, 2024
2 parents 7e7eaf6 + b24c7c5 commit c75ff17
Show file tree
Hide file tree
Showing 10 changed files with 521 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Bump openresty to 1.21.4.3 [PR #1461](https://github.com/3scale/APIcast/pull/1461) [THREESCALE-10601](https://issues.redhat.com/browse/THREESCALE-10601)

- Support Financial-grade API (FAPI) - Baseline profile [PR #1465](https://github.com/3scale/APIcast/pull/1465) [THREESCALE-10973](https://issues.redhat.com/browse/THREESCALE-10973)

## [3.15.0] 2024-04-04

### Fixed
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/man
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/knyar/nginx-lua-prometheus-0.20181120-2.src.rock
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/hamish/lua-resty-iputils-0.3.0-1.src.rock
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/golgote/net-url-0.9-1.src.rock
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/membphis/lua-resty-ipmatcher-0.6.1-0.src.rock

RUN yum -y remove libyaml-devel m4 openssl-devel git gcc luarocks && \
rm -rf /var/cache/yum && yum clean all -y && \
Expand Down
1 change: 1 addition & 0 deletions gateway/Roverfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ liquid 0.2.0-2||production
lua-resty-env 0.4.0-1||production
lua-resty-execvp 0.1.1-1||production
lua-resty-http 0.17.1-0||production
lua-resty-ipmatcher 0.6.1-0||production
lua-resty-iputils 0.3.0-2||production
lua-resty-jit-uuid 0.0.7-2||production
lua-resty-jwt 0.2.0-0||production
Expand Down
1 change: 1 addition & 0 deletions gateway/apicast-scm-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies = {
'penlight',
'nginx-lua-prometheus == 0.20181120',
'lua-resty-jit-uuid',
'lua-resty-ipmatcher',
}
build = {
type = "make",
Expand Down
32 changes: 32 additions & 0 deletions gateway/src/apicast/policy/fapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# FAPI Policy

## Description

The FAPI policy supports various features of the Financial-grade API (FAPI) standard.

## Example configuration

```
"policy_chain": [
{ "name": "apicast.policy.fapi", "configuration": {} },
{
"name": "apicast.policy.apicast"
}
]
```

### Validate x-fapi-customer-ip-address header

```
"policy_chain": [
{
"name": "apicast.policy.fapi",
"configuration": {
"validate_x_fapi_customer_ip_address": true
}
},
{
"name": "apicast.policy.apicast"
}
]
```
18 changes: 18 additions & 0 deletions gateway/src/apicast/policy/fapi/apicast-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "http://apicast.io/policy-v1/schema#manifest#",
"name": "The Financial-grade API (FAPI)",
"summary": "Support FAPI profiles",
"description": ["This policy adding support for Financial-grade API (API) profiles"
],
"version": "builtin",
"configuration": {
"type": "object",
"properties": {
"validate_x_fapi_customer_ip_address": {
"description": "Validate x-fapi-customer-ip-address header",
"type": "boolean",
"default": "false"
}
}
}
}
74 changes: 74 additions & 0 deletions gateway/src/apicast/policy/fapi/fapi.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
--- Financial-grade API (FAPI) policy

local policy = require('apicast.policy')
local _M = policy.new('Financial-grade API (FAPI) Policy', 'builtin')

local uuid = require 'resty.jit-uuid'
local ipmatcher = require "resty.ipmatcher"
local fmt = string.format

local new = _M.new
local X_FAPI_TRANSACTION_ID_HEADER = "x-fapi-transaction-id"
local X_FAPI_CUSTOMER_IP_ADDRESS = "x-fapi-customer-ip-address"

local function is_valid_ip(ip)
if type(ip) ~= "string" then
return false
end
if ipmatcher.parse_ipv4(ip) then
return true
end

return ipmatcher.parse_ipv6(ip)
end

local function error(status_code, msg)
ngx.status = status_code
ngx.header.content_type = 'application/json; charset=utf-8'
ngx.print(fmt('{"error": "%s"}', msg))
ngx.exit(ngx.status)
end

--- Initialize FAPI policy
-- @tparam[config] table config
-- @field[config] validate_x_fapi_customer_ip_address Boolean
function _M.new(config)
local self = new(config)
self.validate_customer_ip_address = config and config.validate_x_fapi_customer_ip_address
return self
end

function _M:access()
--- 6.2.1.13
-- shall not reject requests with a x-fapi-customer-ip-address header containing a valid IPv4 or IPv6 address.
if self.validate_customer_ip_address then
local customer_ip = ngx.req.get_headers()[X_FAPI_CUSTOMER_IP_ADDRESS]

if customer_ip then
-- The standard does not mention the case of having multiple IPs, but the
-- x-fapi-customer-ip-address can contain multiple IPs, however I think it doesn't
-- make much sense for this header to have more than one IP, so we reject the request
-- if the header is a table.
if not is_valid_ip(customer_ip) then
ngx.log(ngx.WARN, "invalid x-fapi-customer-ip-address")
return error(ngx.HTTP_FORBIDDEN, "invalid_request")
end
end
end
end

function _M:header_filter()
--- 6.2.1.11
-- shall set the response header x-fapi-interaction-id to the value received from the corresponding FAPI client request header or to a RFC4122 UUID value if the request header was not provided to track the interaction
local transaction_id = ngx.req.get_headers()[X_FAPI_TRANSACTION_ID_HEADER]
if not transaction_id or transaction_id == "" then
-- Nothing found, generate one
transaction_id = ngx.resp.get_headers()[X_FAPI_TRANSACTION_ID_HEADER]
if not transaction_id or transaction_id == "" then
transaction_id = uuid.generate_v4()
end
end
ngx.header[X_FAPI_TRANSACTION_ID_HEADER] = transaction_id
end

return _M
1 change: 1 addition & 0 deletions gateway/src/apicast/policy/fapi/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require('fapi')
81 changes: 81 additions & 0 deletions spec/policy/fapi/fapi_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
local FAPIPolicy = require('apicast.policy.fapi')
local uuid = require('resty.jit-uuid')

describe('fapi_1_baseline_profile policy', function()
local ngx_req_headers = {}
local ngx_resp_headers = {}
local context = {}
before_each(function()
ngx.header = {}
ngx_req_headers = {}
ngx_resp_headers = {}
context = {}
stub(ngx.req, 'get_headers', function() return ngx_req_headers end)
stub(ngx.req, 'set_header', function(name, value) ngx_req_headers[name] = value end)
stub(ngx.resp, 'get_headers', function() return ngx_resp_headers end)
stub(ngx.resp, 'set_header', function(name, value) ngx_resp_headers[name] = value end)
stub(ngx, 'print')
stub(ngx, 'exit')
end)

describe('.new', function()
it('works without configuration', function()
assert(FAPIPolicy.new({}))
end)
end)

describe('.header_filter', function()
it('Use value from request', function()
ngx_req_headers['x-fapi-transaction-id'] = 'abc'
local transaction_id_policy = FAPIPolicy.new({})
transaction_id_policy:header_filter()
assert.same('abc', ngx.header['x-fapi-transaction-id'])
end)

it('Only use x-fapi-transaction-id from request if the header also exist in response from upstream', function()
ngx_req_headers['x-fapi-transaction-id'] = 'abc'
ngx_resp_headers['x-fapi-transaction-id'] = 'bdf'
local transaction_id_policy = FAPIPolicy.new({})
transaction_id_policy:header_filter()
assert.same('abc', ngx.header['x-fapi-transaction-id'])
end)

it('Use x-fapi-transaction-id from upstream response', function()
ngx_resp_headers['x-fapi-transaction-id'] = 'abc'
local transaction_id_policy = FAPIPolicy.new({})
transaction_id_policy:header_filter()
assert.same('abc', ngx.header['x-fapi-transaction-id'])
end)

it('generate uuid if header does not exist in both request and response', function()
local transaction_id_policy = FAPIPolicy.new({})
transaction_id_policy:header_filter()
assert.is_true(uuid.is_valid(ngx.header['x-fapi-transaction-id']))
end)
end)

describe('x-fapi-customer-ip-address', function()
it('Allow request with valid IPv4', function()
ngx_req_headers['x-fapi-customer-ip-address'] = '127.0.0.1'
local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true})
transaction_id_policy:access()
assert.stub(ngx.exit).was_not.called_with(403)
end)

it('Allow request with valid IPv6', function()
ngx_req_headers['x-fapi-customer-ip-address'] = '2001:db8::123:12:1'
local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true})
transaction_id_policy:access()
assert.stub(ngx.exit).was_not.called_with(403)
end)

it('Reject request if header contains more than 1 IP', function()
ngx_req_headers['x-fapi-customer-ip-address'] = {"2001:db8::123:12:1", "127.0.0.1"}
local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true})
transaction_id_policy:access()
assert.same(ngx.status, 403)
assert.stub(ngx.print).was.called_with('{"error": "invalid_request"}')
assert.stub(ngx.exit).was.called_with(403)
end)
end)
end)
Loading

0 comments on commit c75ff17

Please sign in to comment.