diff --git a/changelog/unreleased/kong/11523.yml b/changelog/unreleased/kong/11523.yml new file mode 100644 index 000000000000..81e8a6847746 --- /dev/null +++ b/changelog/unreleased/kong/11523.yml @@ -0,0 +1,5 @@ +"message": "**CORS**: Support the `Access-Control-Request-Private-Network` header in crossing-origin pre-light requests" +"type": "feature" +"scope": "Plugin" +"jiras": +- "FTI-5258" diff --git a/kong/clustering/compat/removed_fields.lua b/kong/clustering/compat/removed_fields.lua index 4bbdbd99cca0..371621e1d99c 100644 --- a/kong/clustering/compat/removed_fields.lua +++ b/kong/clustering/compat/removed_fields.lua @@ -93,4 +93,10 @@ return { "sync_rate", }, }, + + [3005000000] = { + cors = { + "private_network", + } + } } diff --git a/kong/plugins/cors/handler.lua b/kong/plugins/cors/handler.lua index 0ec37e32976b..5050f4bd022c 100644 --- a/kong/plugins/cors/handler.lua +++ b/kong/plugins/cors/handler.lua @@ -228,6 +228,11 @@ function CorsHandler:access(conf) set_header("Access-Control-Max-Age", tostring(conf.max_age)) end + if conf.private_network and + kong.request.get_header("Access-Control-Request-Private-Network") == 'true' then + set_header("Access-Control-Allow-Private-Network", 'true') + end + return kong.response.exit(HTTP_OK) end diff --git a/kong/plugins/cors/schema.lua b/kong/plugins/cors/schema.lua index 76c26b44ca58..4910a321d085 100644 --- a/kong/plugins/cors/schema.lua +++ b/kong/plugins/cors/schema.lua @@ -46,6 +46,7 @@ return { }, }, }, { max_age = { description = "Indicates how long the results of the preflight request can be cached, in `seconds`.", type = "number" }, }, { credentials = { description = "Flag to determine whether the `Access-Control-Allow-Credentials` header should be sent with `true` as the value.", type = "boolean", required = true, default = false }, }, + { private_network = { description = "Flag to determine whether the `Access-Control-Allow-Private-Network` header should be sent with `true` as the value.", type = "boolean", required = true, default = false }, }, { preflight_continue = { description = "A boolean value that instructs the plugin to proxy the `OPTIONS` preflight request to the Upstream service.", type = "boolean", required = true, default = false }, }, }, }, }, }, diff --git a/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua b/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua index 16335e3609ed..2ea970ec5444 100644 --- a/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua +++ b/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua @@ -35,11 +35,11 @@ end local function get_plugin(node_id, node_version, name) local res, err = cluster_client({ id = node_id, version = node_version }) assert.is_nil(err) - assert.is_table(res and res.config and res.config.plugins, + assert.is_table(res and res.config_table and res.config_table.plugins, "invalid response from clustering client") local plugin - for _, p in ipairs(res.config.plugins) do + for _, p in ipairs(res.config_table.plugins or {}) do if p.name == name then plugin = p break @@ -108,7 +108,7 @@ describe("CP/DP config compat transformations #" .. strategy, function() end) describe("plugin config fields", function() - local rate_limit, opentelemetry, zipkin + local rate_limit, opentelemetry, zipkin, cors lazy_setup(function() rate_limit = admin.plugins:insert { @@ -125,26 +125,38 @@ describe("CP/DP config compat transformations #" .. strategy, function() -- ]] }, } + + cors = admin.plugins:insert { + name = "cors", + enabled = true, + config = { + -- [[ new fields 3.5.0 + private_network = false + -- ]] + } + } end) lazy_teardown(function() admin.plugins:remove({ id = rate_limit.id }) end) - it("removes new fields before sending them to older DP nodes", function() - local id = utils.uuid() - local plugin = get_plugin(id, "3.0.0", rate_limit.name) + local function do_assert(node_id, node_version, expected_entity) + local plugin = get_plugin(node_id, node_version, expected_entity.name) + assert.same(expected_entity.config, plugin.config) + assert.equals(CLUSTERING_SYNC_STATUS.NORMAL, get_sync_status(node_id)) + end + it("removes new fields before sending them to older DP nodes", function() --[[ For 3.0.x should not have: error_code, error_message, sync_rate --]] - local expected = utils.cycle_aware_deep_copy(rate_limit.config) - expected.error_code = nil - expected.error_message = nil - expected.sync_rate = nil - assert.same(expected, plugin.config) - assert.equals(CLUSTERING_SYNC_STATUS.NORMAL, get_sync_status(id)) + local expected = utils.cycle_aware_deep_copy(rate_limit) + expected.config.error_code = nil + expected.config.error_message = nil + expected.config.sync_rate = nil + do_assert(utils.uuid(), "3.0.0", expected) --[[ @@ -152,12 +164,9 @@ describe("CP/DP config compat transformations #" .. strategy, function() should have: error_code, error_message should not have: sync_rate --]] - id = utils.uuid() - plugin = get_plugin(id, "3.2.0", rate_limit.name) - expected = utils.cycle_aware_deep_copy(rate_limit.config) - expected.sync_rate = nil - assert.same(expected, plugin.config) - assert.equals(CLUSTERING_SYNC_STATUS.NORMAL, get_sync_status(id)) + expected = utils.cycle_aware_deep_copy(rate_limit) + expected.config.sync_rate = nil + do_assert(utils.uuid(), "3.2.0", expected) --[[ @@ -165,19 +174,26 @@ describe("CP/DP config compat transformations #" .. strategy, function() should have: error_code, error_message should not have: sync_rate --]] - id = utils.uuid() - plugin = get_plugin(id, "3.3.0", rate_limit.name) - expected = utils.cycle_aware_deep_copy(rate_limit.config) - expected.sync_rate = nil - assert.same(expected, plugin.config) - assert.equals(CLUSTERING_SYNC_STATUS.NORMAL, get_sync_status(id)) + expected = utils.cycle_aware_deep_copy(rate_limit) + expected.config.sync_rate = nil + do_assert(utils.uuid(), "3.3.0", expected) end) it("does not remove fields from DP nodes that are already compatible", function() - local id = utils.uuid() - local plugin = get_plugin(id, "3.4.0", rate_limit.name) - assert.same(rate_limit.config, plugin.config) - assert.equals(CLUSTERING_SYNC_STATUS.NORMAL, get_sync_status(id)) + do_assert(utils.uuid(), "3.4.0", rate_limit) + end) + + describe("compatibility test for cors plugin", function() + it("removes `config.private_network` before sending them to older(less than 3.5.0.0) DP nodes", function() + assert.not_nil(cors.config.private_network) + local expected_cors = utils.cycle_aware_deep_copy(cors) + expected_cors.config.private_network = nil + do_assert(utils.uuid(), "3.4.0", expected_cors) + end) + + it("does not remove `config.private_network` from DP nodes that are already compatible", function() + do_assert(utils.uuid(), "3.5.0", cors) + end) end) describe("compatibility tests for opentelemetry plugin", function() diff --git a/spec/03-plugins/13-cors/01-access_spec.lua b/spec/03-plugins/13-cors/01-access_spec.lua index fcdbfa510186..7113948c57af 100644 --- a/spec/03-plugins/13-cors/01-access_spec.lua +++ b/spec/03-plugins/13-cors/01-access_spec.lua @@ -283,6 +283,10 @@ for _, strategy in helpers.each_strategy() do hosts = { "cors12.com" }, }) + local route13 = bp.routes:insert({ + hosts = { "cors13.com" }, + }) + local mock_upstream = bp.services:insert { host = helpers.mock_upstream_hostname, port = helpers.mock_upstream_port, @@ -437,6 +441,16 @@ for _, strategy in helpers.each_strategy() do } } + bp.plugins:insert { + name = "cors", + route = { id = route13.id }, + config = { + preflight_continue = false, + private_network = true, + origins = { 'allowed-domain.test' } + } + } + bp.plugins:insert { name = "cors", route = { id = route_timeout.id }, @@ -705,6 +719,20 @@ for _, strategy in helpers.each_strategy() do assert.res_status(200, res) assert.is_nil(res.headers["Access-Control-Allow-Origin"]) end) + + it("support private-network", function() + local res = assert(proxy_client:send { + method = "OPTIONS", + headers = { + ["Host"] = "cors13.com", + ["Origin"] = "allowed-domain.test", + ["Access-Control-Request-Private-Network"] = "true", + ["Access-Control-Request-Method"] = "PUT", + } + }) + assert.res_status(200, res) + assert.equal("true", res.headers["Access-Control-Allow-Private-Network"]) + end) end) describe("HTTP method: others", function()