Skip to content

Commit

Permalink
[fapi] Add option to validate x-fapi-customer-ip-address header
Browse files Browse the repository at this point in the history
  • Loading branch information
tkan145 committed Jun 7, 2024
1 parent e83e17f commit b24c7c5
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 2 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
16 changes: 16 additions & 0 deletions gateway/src/apicast/policy/fapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@ The FAPI policy supports various features of the Financial-grade API (FAPI) stan
}
]
```

### 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"
}
]
```
8 changes: 7 additions & 1 deletion gateway/src/apicast/policy/fapi/apicast-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"version": "builtin",
"configuration": {
"type": "object",
"properties": {}
"properties": {
"validate_x_fapi_customer_ip_address": {
"description": "Validate x-fapi-customer-ip-address header",
"type": "boolean",
"default": "false"
}
}
}
}
47 changes: 46 additions & 1 deletion gateway/src/apicast/policy/fapi/fapi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,62 @@ 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()
-- Get x-fapi-transaction-id from the request
--- 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
Expand Down
27 changes: 27 additions & 0 deletions spec/policy/fapi/fapi_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ describe('fapi_1_baseline_profile policy', function()
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()
Expand Down Expand Up @@ -51,4 +53,29 @@ describe('fapi_1_baseline_profile policy', function()
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)
104 changes: 104 additions & 0 deletions t/apicast-policy-fapi.t
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,107 @@ x-fapi-transaction-id: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-
--- error_code: 200
--- no_error_log
[error]



=== TEST 5: Validate x-fapi-customer-ip-address header
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 }
],
"policy_chain": [
{
"name": "apicast.policy.fapi",
"configuration": {
"validate_x_fapi_customer_ip_address": true
}
},
{
"name": "apicast.policy.apicast"
}
]
}
}
]
}
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
ngx.exit(200)
}
}
--- upstream
location / {
content_by_lua_block {
ngx.exit(200)
}
}
--- more_headers
x-fapi-customer-ip-address: 192.168.0.1
--- request
GET /?user_key=value
--- error_code: 200
--- no_error_log
[error]



=== TEST 6: Reject request with invalid x-fapi-customer-ip-address header
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 }
],
"policy_chain": [
{
"name": "apicast.policy.fapi",
"configuration": {
"validate_x_fapi_customer_ip_address": true
}
},
{
"name": "apicast.policy.apicast"
}
]
}
}
]
}
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
ngx.exit(200)
}
}
--- upstream
location / {
content_by_lua_block {
ngx.exit(200)
}
}
--- more_headers
x-fapi-customer-ip-address: something
--- request
GET /?user_key=value
--- error_code: 403
--- response_body chomp
{"error": "invalid_request"}
--- no_error_log
[error]

0 comments on commit b24c7c5

Please sign in to comment.