lua_spec.ingress_rbac_logging_spec.lua Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of envoy-control-tests Show documentation
Show all versions of envoy-control-tests Show documentation
Production-ready Control Plane for Service Mesh based on Envoy Proxy.
require('ingress_rbac_logging')
local _ = match._
local contains = function(substring)
return match.matches(substring, nil, true)
end
local function formatLog(method, path, source_ip, client_name, protocol, request_id, status_code, trusted_client, allowed_client, rbac_action, authority, lua_authority, jwt_token_status, headers_to_log)
local message = "\nINCOMING_PERMISSIONS {\"method\":\"" .. method ..
"\",\"path\":\"" .. path ..
"\",\"clientIp\":\"" .. source_ip ..
"\",\"clientName\":\"" .. escape(client_name) ..
"\",\"trustedClient\":" .. tostring(trusted_client) ..
",\"authority\":\"" .. escape(authority) ..
"\",\"luaDestinationAuthority\":\"" .. escape(lua_authority) ..
"\",\"clientAllowedToAllEndpoints\":" .. tostring(allowed_client) ..
",\"protocol\":\"" .. protocol ..
"\",\"requestId\":\"" .. escape(request_id) ..
"\",\"statusCode\":" .. status_code ..
",\"rbacAction\":\"" .. rbac_action ..
"\",\"jwtTokenStatus\":\"" .. jwt_token_status .. "\""
if headers_to_log then
for k, v in pairs(headers_to_log) do
message = message .. ',"' .. k .. '":"' .. v .. '"'
end
end
return message .. '}'
end
local function handlerMock(headers, dynamic_metadata, https, filter_metadata, logs)
local metadata_mock = mock({
set = function()
end,
get = function(_, key)
return dynamic_metadata[key]
end
})
local log_info_mock = spy(function()
end)
return {
headers = function()
return {
get = function(_, key)
assert.is.not_nil(key, "headers:get() called with nil argument")
return headers[key]
end,
add = function(_, key, value)
headers[key] = value
end
}
end,
streamInfo = function()
return {
dynamicMetadata = function()
return metadata_mock
end,
}
end,
connection = function()
return {
ssl = function()
return https or nil
end
}
end,
logInfo = log_info_mock,
metadata = function()
return {
get = function(_, key)
return filter_metadata[key]
end
}
end
}
end
describe("json escape string:", function()
local chars_to_escape = {
["\\"] = "\\\\",
["\""] = "\\\"",
["\b"] = "\\b",
["\f"] = "\\f",
["\n"] = "\\n",
["\r"] = "\\r",
["\t"] = "\\t",
['some \t text'] = 'some \\t text',
['multiple \" escaped \t text'] = 'multiple \\" escaped \\t text',
['/multiple \" escaped \t text/'] = '/multiple \\" escaped \\t text/',
['no escape here'] = 'no escape here',
["{\"hello\": \"world\"}"] = '{\\"hello\\": \\"world\\"}',
}
for given, expected in pairs(chars_to_escape) do
it("should escape '" .. given .. "' with backslashes", function()
-- when
local escaped = escape(given)
assert.equals(expected, escaped)
end)
end
end)
describe("envoy_on_request:", function()
it("should set dynamic metadata", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-service-name'] = 'lorem-service',
['x-forwarded-for'] = "127.0.4.3",
[':authority'] = "authority",
['x-lua-destination-authority'] = "lua_authority"
}
local filter_metadata = {
['client_identity_headers'] = { 'x-service-name' }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.path", "/path")
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.method", "GET")
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", "lorem-service")
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.xff_header", "127.0.4.3")
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.authority", "authority")
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.lua_destination_authority", "lua_authority")
end)
it("should set dynamic metadata for request id", function()
-- given
local headers = {
['x-request-id'] = '123-456-789',
}
local filter_metadata = {
['request_id_headers'] = { 'x-request-id' }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.request_id", "123-456-789")
end)
it("should set allowed_client for defined client", function()
-- given
local headers = {
['x-service-name'] = 'allowed_client'
}
local filter_metadata = {
['client_identity_headers'] = { 'x-service-name' },
['clients_allowed_to_all_endpoints'] = { 'allowed_client' }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.allowed_client", true)
end)
it("should set allowed_client to false for unknown client", function()
-- given
local headers = {
['x-service-name'] = 'not_allowed_client'
}
local filter_metadata = {
['client_identity_headers'] = { 'x-service-name' },
['clients_allowed_to_all_endpoints'] = { 'allowed_client' }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.allowed_client", false)
end)
it("should set client_name from x-client-name-trusted header", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-service-name'] = 'lorem-service',
['x-client-name-trusted'] = 'service-first,service-second'
}
local filter_metadata = {
['client_identity_headers'] = { "x-service-name" },
['trusted_client_identity_header'] = "x-client-name-trusted"
}
local handle = handlerMock(headers, {}, true, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", 'service-first,service-second')
end)
it("should add not trusted to client_name if ssl available and name was not from certificate", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-service-name'] = 'lorem-service',
}
local filter_metadata = {
['client_identity_headers'] = { "x-service-name" },
['trusted_client_identity_header'] = "x-client-name-trusted"
}
local handle = handlerMock(headers, {}, true, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", 'lorem-service (not trusted)')
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.trusted_client", false)
end)
it("should set client_name metadata using data from configured headers", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-service-name'] = 'lorem-service',
['x-forwarded-for'] = "127.0.4.3"
}
local filter_metadata = {
['client_identity_headers'] = { "x-service-name", "x-forwarded-for" }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", "lorem-service")
end)
it("should set client_name metadata using second configured header when first one is missing", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-forwarded-for'] = "127.0.4.3"
}
local filter_metadata = {
['client_identity_headers'] = { "x-service-name", "x-forwarded-for" }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", "127.0.4.3")
end)
it("should set empty client_name when there are empty client_identity_headers configured", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-forwarded-for'] = "127.0.4.3"
}
local filter_metadata = {
['client_identity_headers'] = {}
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", "")
end)
it("should set empty client_name when there are no client_identity_headers configured", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-forwarded-for'] = "127.0.4.3"
}
local filter_metadata = {}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", "")
end)
it("should set empty client_name when there are no headers matching client_identity_headers", function()
-- given
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
['x-forwarded-for'] = "127.0.4.3"
}
local filter_metadata = {
['client_identity_headers'] = { "x-service-name", "x-via-ip" }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.client_name", "")
end)
it("should survive lack of trusted_client_identity_header metadata", function()
-- given
local empty_metadata = {}
local headers = {
[':path'] = '/path',
[':method'] = 'GET',
}
local handle = handlerMock(headers, {}, nil, empty_metadata)
local dynamic_metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(dynamic_metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.path", "/path")
assert.spy(dynamic_metadata.set).was_called_with(_, "envoy.filters.http.lua", "request.info.method", "GET")
end)
it('should set dynamic metadata for custom headers', function()
-- given
local headers = {
['test-header'] = 'header-value'
}
local filter_metadata = {
['rbac_headers_to_log'] = { 'test-header' }
}
local handle = handlerMock(headers, {}, nil, filter_metadata)
local metadata = handle:streamInfo():dynamicMetadata()
-- when
envoy_on_request(handle)
-- then
assert.spy(metadata.set).was_called_with(_, 'envoy.filters.http.lua', 'request.info.headers.test-header', 'header-value')
end)
end)
describe("envoy_on_response:", function()
local headers
local dynamic_metadata
local ssl
local metadata
before_each(function()
headers = {
[':status'] = '403'
}
dynamic_metadata = {
['envoy.filters.http.jwt_authn'] = {
['jwt_failure_reason'] = {
['code'] = '3',
['message'] = 'Jwt is expired'
}
},
['envoy.filters.http.rbac'] = {
['shadow_engine_result'] = 'denied'
},
['envoy.filters.http.lua'] = {
['service_name'] = "service",
['request.info.client_name'] = 'service-first',
['request.info.path'] = '/path?query=val',
['request.info.method'] = 'POST',
['request.info.xff_header'] = '127.1.1.3',
['request.info.authority'] = 'authority',
['request.info.lua_destination_authority'] = 'lua_authority'
}
}
ssl = true
metadata = {}
end)
describe("should log unauthorized requests:", function()
it("https request", function()
-- given
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"POST",
"/path?query=val",
"127.1.1.3",
"service-first",
"https",
"",
"403",
false,
false,
"denied",
"authority",
"lua_authority",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
it("http request", function()
-- given
ssl = false
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"POST",
"/path?query=val",
"127.1.1.3",
"service-first",
"http",
"",
"403",
false,
false,
"denied",
"authority",
"lua_authority",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
it("as logged when status code is different than 403", function()
-- given
headers[':status'] = '503'
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"POST",
"/path?query=val",
"127.1.1.3",
"service-first",
"https",
"",
"503",
false,
false,
"shadow_denied",
"authority",
"lua_authority",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
it("allowed & logged request", function()
-- given
headers[':status'] = '200'
headers['x-envoy-upstream-service-time'] = '10'
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"POST",
"/path?query=val",
"127.1.1.3",
"service-first",
"https",
"",
"200",
false,
false,
"shadow_denied",
"authority",
"lua_authority",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
it("request with no lua filter metadata fields saved", function()
-- given
dynamic_metadata['envoy.filters.http.lua'] = {}
headers = {}
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"",
"",
"",
"",
"https",
"",
"0",
false,
false,
"shadow_denied",
"",
"",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
it("request with no lua filter metadata saved", function()
-- given
dynamic_metadata['envoy.filters.http.lua'] = nil
headers = {}
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"",
"",
"",
"",
"https",
"",
"0",
false,
false,
"shadow_denied",
"",
"",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
it("request with empty path", function()
-- given
dynamic_metadata['envoy.filters.http.lua']['request.info.path'] = ''
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"POST",
"",
"127.1.1.3",
"service-first",
"https",
"",
"403",
false,
false,
"denied",
"authority",
"lua_authority",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
it('with populated header values', function()
-- given
dynamic_metadata['envoy.filters.http.lua']['request.info.headers.test-header'] = 'test-value'
metadata['rbac_headers_to_log'] = { 'test-header' }
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"POST",
"/path?query=val",
"127.1.1.3",
"service-first",
"https",
"",
"403",
false,
false,
"denied",
"authority",
"lua_authority",
"Jwt is expired",
{ ['test-header'] = 'test-value' }
))
assert.spy(handle.logInfo).was_called(1)
end)
end)
describe("should log requests:", function()
it("with globally allowed client", function()
-- given
dynamic_metadata['envoy.filters.http.rbac']['shadow_engine_result'] = 'denied'
dynamic_metadata['envoy.filters.http.lua']['request.info.allowed_client'] = true
headers['x-envoy-upstream-service-time'] = '10'
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, formatLog(
"POST",
"/path?query=val",
"127.1.1.3",
"service-first",
"https",
"",
"403",
false,
true,
"shadow_denied",
"authority",
"lua_authority",
"Jwt is expired"
))
assert.spy(handle.logInfo).was_called(1)
end)
end)
describe("should not log requests:", function()
it("request with no rbac metadata", function()
-- given
dynamic_metadata = {}
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_not_called()
end)
it("authorized request", function()
-- given
dynamic_metadata['envoy.filters.http.rbac']['shadow_engine_result'] = 'allowed'
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_not_called()
end)
end)
describe("should handle x-forwarded-for formats:", function()
local xff_to_expected_client_ip = {
{ "", "" },
{ "127.9.3.2", "127.9.3.2" },
{ "3.23.2.44 , 2.34.3.2,127.1.3.5", "127.1.3.5" },
{ "2001:db8:85a3:8d3:1319:8a2e:370:7348,1001:db8:85a3:8d3:1319:8a2e:370:2222", "1001:db8:85a3:8d3:1319:8a2e:370:2222" },
{ "2001:db8:85a3:8d3:1319:8a2e:370:7348,127.1.3.4", "127.1.3.4" }
}
for i, v in ipairs(xff_to_expected_client_ip) do
local xff = v[1]
local expected_client_ip = v[2]
it("'" .. xff .. "' -> '" .. expected_client_ip .. "'", function()
-- given
dynamic_metadata['envoy.filters.http.lua']['request.info.xff_header'] = xff
local handle = handlerMock(headers, dynamic_metadata, ssl, metadata)
-- when
envoy_on_response(handle)
-- then
assert.spy(handle.logInfo).was_called_with(_, contains('"clientIp":"' .. expected_client_ip .. '"'))
end)
end
end)
end)
--[[
tools:
show spy calls:
require 'pl.pretty'.dump(handle.logInfo.calls, "/dev/stderr")
]] --