Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

THREESCALE-10973 - Support Financial-grade API (FAPI) - Baseline profile #1465

Merged
merged 2 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading