Skip to content

Commit 0733e3d

Browse files
committed
fix(schema): deprecated shorthand fields precedence
The shorthand fields e.g. as used in (the url is a shorthand) take precedence: ``` http PUT :8001/services/test url=http://test.org/ port=2345 ``` That means that `port=2345` will be overwritten by http default port of `80` that is implicit in shorthand field `url`. This is how it has been for a long long time. In PR #12686 we added `deprecation` field to shorthand fields definition. Some of our automatic migrations without database migrations magic use this functionality, but the precedence there does not good: ``` http -f PUT :8001/plugins/x name=x config.new=test config.old=should_be_ignored ``` In above the `config.old` is a shorthand field with a deprecation property. Thus it should not overwrite the `config.new`, but in current code base it does. This PR changes it so that it doesn't do it anymore. KAG-5134 and https://kongstrong.slack.com/archives/C07AQH7SAF8/p1722589141558609 Signed-off-by: Aapo Talvensaari <[email protected]>
1 parent f658430 commit 0733e3d

File tree

5 files changed

+117
-56
lines changed

5 files changed

+117
-56
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
message: Deprecated shorthand fields don't take precedence over replacement fields when both are specified.
2+
type: bugfix
3+
scope: Core

kong/api/routes/plugins.lua

+13-35
Original file line numberDiff line numberDiff line change
@@ -83,28 +83,23 @@ end
8383

8484
local function patch_plugin(self, db, _, parent)
8585
local post = self.args and self.args.post
86+
if post then
87+
-- Read-before-write only if necessary
88+
if post.name == nil then
89+
-- We need the name, otherwise we don't know what type of
90+
-- plugin this is and we can't perform *any* validations.
91+
local plugin, _, err_t = endpoints.select_entity(self, db, db.plugins.schema)
92+
if err_t then
93+
return endpoints.handle_error(err_t)
94+
end
8695

87-
-- Read-before-write only if necessary
88-
if post and (post.name == nil or
89-
post.route == nil or
90-
post.service == nil or
91-
post.consumer == nil) then
92-
93-
-- We need the name, otherwise we don't know what type of
94-
-- plugin this is and we can't perform *any* validations.
95-
local plugin, _, err_t = endpoints.select_entity(self, db, db.plugins.schema)
96-
if err_t then
97-
return endpoints.handle_error(err_t)
98-
end
96+
if not plugin then
97+
return kong.response.exit(404, { message = "Not found" })
98+
end
9999

100-
if not plugin then
101-
return kong.response.exit(404, { message = "Not found" })
100+
post.name = plugin.name
102101
end
103102

104-
plugin = plugin or {}
105-
106-
post.name = post.name or plugin.name
107-
108103
-- Only now we can decode the 'config' table for form-encoded values
109104
local content_type = ngx.var.content_type
110105
if content_type then
@@ -115,23 +110,6 @@ local function patch_plugin(self, db, _, parent)
115110
end
116111
end
117112

118-
-- While we're at it, get values for composite uniqueness check
119-
post.route = post.route or plugin.route
120-
post.service = post.service or plugin.service
121-
post.consumer = post.consumer or plugin.consumer
122-
123-
if not post.route and self.params.routes then
124-
post.route = { id = self.params.routes }
125-
end
126-
127-
if not post.service and self.params.services then
128-
post.service = { id = self.params.services }
129-
end
130-
131-
if not post.consumer and self.params.consumers then
132-
post.consumer = { id = self.params.consumers }
133-
end
134-
135113
self.args.post = post
136114
end
137115

kong/db/dao/plugins.lua

+35-19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ local Plugins = {}
2323

2424

2525
local fmt = string.format
26+
local type = type
2627
local null = ngx.null
2728
local pairs = pairs
2829
local tostring = tostring
@@ -75,9 +76,8 @@ local function check_protocols_match(self, plugin)
7576
})
7677
return nil, tostring(err_t), err_t
7778
end
78-
end
7979

80-
if type(plugin.service) == "table" then
80+
elseif type(plugin.service) == "table" then
8181
if not has_common_protocol_with_service(self, plugin, plugin.service) then
8282
local err_t = self.errors:schema_violation({
8383
protocols = "must match the protocols of at least one route " ..
@@ -95,9 +95,9 @@ local function check_ordering_validity(self, entity)
9595
--[[
9696
Plugins that are scoped to a consumer can't be a target for reordering
9797
because they rely on a context (ngx.authenticated_consumer) which is only
98-
set during the access phase of an authentacation plugin. This means that
99-
we can't influence the order of plugins without running their access phase first
100-
which is a catch-22.
98+
set during the access phase of an authentication plugin. This means that
99+
we can't influence the order of plugins without running their access phase
100+
first which is a catch-22.
101101
--]]
102102
if type(entity.ordering) ~= "table" then
103103
-- no reordering requested, no need to validate further
@@ -132,22 +132,38 @@ end
132132

133133

134134
function Plugins:update(primary_key, entity, options)
135-
options = options or {}
136-
options.hide_shorthands = true
137-
local rbw_entity = self.super.select(self, primary_key, options) -- ignore errors
138-
if rbw_entity then
139-
entity = self.schema:merge_values(entity, rbw_entity)
140-
end
141-
local ok, err, err_t = check_protocols_match(self, entity)
142-
if not ok then
143-
return nil, err, err_t
144-
end
145-
local ok_o, err_o, err_t_o = check_ordering_validity(self, entity)
146-
if not ok_o then
147-
return nil, err_o, err_t_o
135+
local rbw_entity
136+
if entity.protocols or entity.service or entity.route then
137+
if (entity.protocols and not entity.route)
138+
or (entity.service and not entity.protocols)
139+
or (entity.route and not entity.protocols)
140+
then
141+
rbw_entity = self.super.select(self, primary_key, options)
142+
if rbw_entity then
143+
entity.protocols = entity.protocols or rbw_entity.protocols
144+
entity.service = entity.service or rbw_entity.service
145+
entity.route = entity.route or rbw_entity.route
146+
end
147+
rbw_entity = rbw_entity or {}
148+
end
149+
local ok, err, err_t = check_protocols_match(self, entity)
150+
if not ok then
151+
return nil, err, err_t
152+
end
148153
end
154+
if entity.ordering or entity.consumer then
155+
if not (rbw_entity or (entity.ordering and entity.consumer)) then
156+
rbw_entity = self.super.select(self, primary_key, options) or {}
157+
end
158+
159+
entity.ordering = entity.ordering or rbw_entity.ordering
160+
entity.consumer = entity.consumer or rbw_entity.consumer
149161

150-
options.hide_shorthands = false
162+
local ok_o, err_o, err_t_o = check_ordering_validity(self, entity)
163+
if not ok_o then
164+
return nil, err_o, err_t_o
165+
end
166+
end
151167
return self.super.update(self, primary_key, entity, options)
152168
end
153169

kong/db/schema/init.lua

+12-2
Original file line numberDiff line numberDiff line change
@@ -1758,14 +1758,24 @@ function Schema:process_auto_fields(data, context, nulls, opts)
17581758
local read_only_data = cycle_aware_deep_copy(data)
17591759
local new_values = sdata.func(value, read_only_data)
17601760
if new_values then
1761+
-- a shorthand field may have a deprecation property, that is used
1762+
-- to determine whether the shorthand's return value takes precedence
1763+
-- over the similarly named actual schema fields' value when both
1764+
-- are present. On deprecated shorthand fields the actual schema
1765+
-- field value takes the precedence, otherwise the shorthand's
1766+
-- return value takes the precedence.
1767+
local deprecation = sdata.deprecation
17611768
for k, v in pairs(new_values) do
17621769
if type(v) == "table" then
17631770
local source = {}
1764-
if data[k] and data[k] ~= ngx.null then
1771+
if data[k] and data[k] ~= null then
17651772
source = data[k]
17661773
end
1774+
data[k] = deprecation and table_merge(v, source)
1775+
or table_merge(source, v)
1776+
17671777
data[k] = table_merge(source, v)
1768-
else
1778+
elseif not deprecation or data[k] == nil then
17691779
data[k] = v
17701780
end
17711781
end

spec/01-unit/01-db/01-schema/01-schema_spec.lua

+54
Original file line numberDiff line numberDiff line change
@@ -4251,6 +4251,60 @@ describe("schema", function()
42514251
assert.same({ name = "test1" }, output)
42524252
end)
42534253

4254+
it("takes precedence", function()
4255+
local TestSchema = Schema.new({
4256+
name = "test",
4257+
fields = {
4258+
{ name = { type = "string" } },
4259+
},
4260+
shorthand_fields = {
4261+
{
4262+
username = {
4263+
type = "string",
4264+
func = function(value)
4265+
return {
4266+
name = value
4267+
}
4268+
end,
4269+
},
4270+
},
4271+
},
4272+
})
4273+
4274+
local input = { username = "test1", name = "ignored" }
4275+
local output, _ = TestSchema:process_auto_fields(input)
4276+
assert.same({ name = "test1" }, output)
4277+
end)
4278+
4279+
it("does not take precedence if deprecated", function()
4280+
local TestSchema = Schema.new({
4281+
name = "test",
4282+
fields = {
4283+
{ name = { type = "string" } },
4284+
},
4285+
shorthand_fields = {
4286+
{
4287+
username = {
4288+
type = "string",
4289+
func = function(value)
4290+
return {
4291+
name = value
4292+
}
4293+
end,
4294+
deprecation = {
4295+
message = "username is deprecated, please use name instead",
4296+
removal_in_version = "4.0",
4297+
},
4298+
},
4299+
},
4300+
},
4301+
})
4302+
4303+
local input = { username = "ignored", name = "test1" }
4304+
local output, _ = TestSchema:process_auto_fields(input)
4305+
assert.same({ name = "test1" }, output)
4306+
end)
4307+
42544308
it("can produce multiple fields", function()
42554309
local TestSchema = Schema.new({
42564310
name = "test",

0 commit comments

Comments
 (0)