Skip to content

Commit c75ff17

Browse files
authored
Merge pull request #1465 from tkan145/THREESCALE-10973-fapi-baseline
THREESCALE-10973 - Support Financial-grade API (FAPI) - Baseline profile
2 parents 7e7eaf6 + b24c7c5 commit c75ff17

File tree

10 files changed

+521
-0
lines changed

10 files changed

+521
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1515

1616
- 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)
1717

18+
- 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)
19+
1820
## [3.15.0] 2024-04-04
1921

2022
### Fixed

Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/man
6060
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/knyar/nginx-lua-prometheus-0.20181120-2.src.rock
6161
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/hamish/lua-resty-iputils-0.3.0-1.src.rock
6262
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/golgote/net-url-0.9-1.src.rock
63+
RUN luarocks install --deps-mode=none --tree /usr/local https://luarocks.org/manifests/membphis/lua-resty-ipmatcher-0.6.1-0.src.rock
6364

6465
RUN yum -y remove libyaml-devel m4 openssl-devel git gcc luarocks && \
6566
rm -rf /var/cache/yum && yum clean all -y && \

gateway/Roverfile.lock

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ liquid 0.2.0-2||production
1010
lua-resty-env 0.4.0-1||production
1111
lua-resty-execvp 0.1.1-1||production
1212
lua-resty-http 0.17.1-0||production
13+
lua-resty-ipmatcher 0.6.1-0||production
1314
lua-resty-iputils 0.3.0-2||production
1415
lua-resty-jit-uuid 0.0.7-2||production
1516
lua-resty-jwt 0.2.0-0||production

gateway/apicast-scm-1.rockspec

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies = {
2323
'penlight',
2424
'nginx-lua-prometheus == 0.20181120',
2525
'lua-resty-jit-uuid',
26+
'lua-resty-ipmatcher',
2627
}
2728
build = {
2829
type = "make",
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# FAPI Policy
2+
3+
## Description
4+
5+
The FAPI policy supports various features of the Financial-grade API (FAPI) standard.
6+
7+
## Example configuration
8+
9+
```
10+
"policy_chain": [
11+
{ "name": "apicast.policy.fapi", "configuration": {} },
12+
{
13+
"name": "apicast.policy.apicast"
14+
}
15+
]
16+
```
17+
18+
### Validate x-fapi-customer-ip-address header
19+
20+
```
21+
"policy_chain": [
22+
{
23+
"name": "apicast.policy.fapi",
24+
"configuration": {
25+
"validate_x_fapi_customer_ip_address": true
26+
}
27+
},
28+
{
29+
"name": "apicast.policy.apicast"
30+
}
31+
]
32+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "http://apicast.io/policy-v1/schema#manifest#",
3+
"name": "The Financial-grade API (FAPI)",
4+
"summary": "Support FAPI profiles",
5+
"description": ["This policy adding support for Financial-grade API (API) profiles"
6+
],
7+
"version": "builtin",
8+
"configuration": {
9+
"type": "object",
10+
"properties": {
11+
"validate_x_fapi_customer_ip_address": {
12+
"description": "Validate x-fapi-customer-ip-address header",
13+
"type": "boolean",
14+
"default": "false"
15+
}
16+
}
17+
}
18+
}
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
--- Financial-grade API (FAPI) policy
2+
3+
local policy = require('apicast.policy')
4+
local _M = policy.new('Financial-grade API (FAPI) Policy', 'builtin')
5+
6+
local uuid = require 'resty.jit-uuid'
7+
local ipmatcher = require "resty.ipmatcher"
8+
local fmt = string.format
9+
10+
local new = _M.new
11+
local X_FAPI_TRANSACTION_ID_HEADER = "x-fapi-transaction-id"
12+
local X_FAPI_CUSTOMER_IP_ADDRESS = "x-fapi-customer-ip-address"
13+
14+
local function is_valid_ip(ip)
15+
if type(ip) ~= "string" then
16+
return false
17+
end
18+
if ipmatcher.parse_ipv4(ip) then
19+
return true
20+
end
21+
22+
return ipmatcher.parse_ipv6(ip)
23+
end
24+
25+
local function error(status_code, msg)
26+
ngx.status = status_code
27+
ngx.header.content_type = 'application/json; charset=utf-8'
28+
ngx.print(fmt('{"error": "%s"}', msg))
29+
ngx.exit(ngx.status)
30+
end
31+
32+
--- Initialize FAPI policy
33+
-- @tparam[config] table config
34+
-- @field[config] validate_x_fapi_customer_ip_address Boolean
35+
function _M.new(config)
36+
local self = new(config)
37+
self.validate_customer_ip_address = config and config.validate_x_fapi_customer_ip_address
38+
return self
39+
end
40+
41+
function _M:access()
42+
--- 6.2.1.13
43+
-- shall not reject requests with a x-fapi-customer-ip-address header containing a valid IPv4 or IPv6 address.
44+
if self.validate_customer_ip_address then
45+
local customer_ip = ngx.req.get_headers()[X_FAPI_CUSTOMER_IP_ADDRESS]
46+
47+
if customer_ip then
48+
-- The standard does not mention the case of having multiple IPs, but the
49+
-- x-fapi-customer-ip-address can contain multiple IPs, however I think it doesn't
50+
-- make much sense for this header to have more than one IP, so we reject the request
51+
-- if the header is a table.
52+
if not is_valid_ip(customer_ip) then
53+
ngx.log(ngx.WARN, "invalid x-fapi-customer-ip-address")
54+
return error(ngx.HTTP_FORBIDDEN, "invalid_request")
55+
end
56+
end
57+
end
58+
end
59+
60+
function _M:header_filter()
61+
--- 6.2.1.11
62+
-- 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
63+
local transaction_id = ngx.req.get_headers()[X_FAPI_TRANSACTION_ID_HEADER]
64+
if not transaction_id or transaction_id == "" then
65+
-- Nothing found, generate one
66+
transaction_id = ngx.resp.get_headers()[X_FAPI_TRANSACTION_ID_HEADER]
67+
if not transaction_id or transaction_id == "" then
68+
transaction_id = uuid.generate_v4()
69+
end
70+
end
71+
ngx.header[X_FAPI_TRANSACTION_ID_HEADER] = transaction_id
72+
end
73+
74+
return _M
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
return require('fapi')

spec/policy/fapi/fapi_spec.lua

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
local FAPIPolicy = require('apicast.policy.fapi')
2+
local uuid = require('resty.jit-uuid')
3+
4+
describe('fapi_1_baseline_profile policy', function()
5+
local ngx_req_headers = {}
6+
local ngx_resp_headers = {}
7+
local context = {}
8+
before_each(function()
9+
ngx.header = {}
10+
ngx_req_headers = {}
11+
ngx_resp_headers = {}
12+
context = {}
13+
stub(ngx.req, 'get_headers', function() return ngx_req_headers end)
14+
stub(ngx.req, 'set_header', function(name, value) ngx_req_headers[name] = value end)
15+
stub(ngx.resp, 'get_headers', function() return ngx_resp_headers end)
16+
stub(ngx.resp, 'set_header', function(name, value) ngx_resp_headers[name] = value end)
17+
stub(ngx, 'print')
18+
stub(ngx, 'exit')
19+
end)
20+
21+
describe('.new', function()
22+
it('works without configuration', function()
23+
assert(FAPIPolicy.new({}))
24+
end)
25+
end)
26+
27+
describe('.header_filter', function()
28+
it('Use value from request', function()
29+
ngx_req_headers['x-fapi-transaction-id'] = 'abc'
30+
local transaction_id_policy = FAPIPolicy.new({})
31+
transaction_id_policy:header_filter()
32+
assert.same('abc', ngx.header['x-fapi-transaction-id'])
33+
end)
34+
35+
it('Only use x-fapi-transaction-id from request if the header also exist in response from upstream', function()
36+
ngx_req_headers['x-fapi-transaction-id'] = 'abc'
37+
ngx_resp_headers['x-fapi-transaction-id'] = 'bdf'
38+
local transaction_id_policy = FAPIPolicy.new({})
39+
transaction_id_policy:header_filter()
40+
assert.same('abc', ngx.header['x-fapi-transaction-id'])
41+
end)
42+
43+
it('Use x-fapi-transaction-id from upstream response', function()
44+
ngx_resp_headers['x-fapi-transaction-id'] = 'abc'
45+
local transaction_id_policy = FAPIPolicy.new({})
46+
transaction_id_policy:header_filter()
47+
assert.same('abc', ngx.header['x-fapi-transaction-id'])
48+
end)
49+
50+
it('generate uuid if header does not exist in both request and response', function()
51+
local transaction_id_policy = FAPIPolicy.new({})
52+
transaction_id_policy:header_filter()
53+
assert.is_true(uuid.is_valid(ngx.header['x-fapi-transaction-id']))
54+
end)
55+
end)
56+
57+
describe('x-fapi-customer-ip-address', function()
58+
it('Allow request with valid IPv4', function()
59+
ngx_req_headers['x-fapi-customer-ip-address'] = '127.0.0.1'
60+
local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true})
61+
transaction_id_policy:access()
62+
assert.stub(ngx.exit).was_not.called_with(403)
63+
end)
64+
65+
it('Allow request with valid IPv6', function()
66+
ngx_req_headers['x-fapi-customer-ip-address'] = '2001:db8::123:12:1'
67+
local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true})
68+
transaction_id_policy:access()
69+
assert.stub(ngx.exit).was_not.called_with(403)
70+
end)
71+
72+
it('Reject request if header contains more than 1 IP', function()
73+
ngx_req_headers['x-fapi-customer-ip-address'] = {"2001:db8::123:12:1", "127.0.0.1"}
74+
local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true})
75+
transaction_id_policy:access()
76+
assert.same(ngx.status, 403)
77+
assert.stub(ngx.print).was.called_with('{"error": "invalid_request"}')
78+
assert.stub(ngx.exit).was.called_with(403)
79+
end)
80+
end)
81+
end)

0 commit comments

Comments
 (0)