Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apisix/admin/consumers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ local core = require("apisix.core")
local plugins = require("apisix.admin.plugins")
local plugins_encrypt_conf = require("apisix.admin.plugins").encrypt_conf
local resource = require("apisix.admin.resource")
local apisix_consumer = require("apisix.consumer")


local function check_conf(username, conf, need_username, schema, opts)
Expand All @@ -36,6 +37,13 @@ local function check_conf(username, conf, need_username, schema, opts)
if not ok then
return nil, {error_msg = "invalid plugins configuration: " .. err}
end

if not opts.skip_references_check then
ok, err = apisix_consumer.check_duplicate_key(conf.plugins, conf.username)
if not ok then
return nil, {error_msg = err}
end
end
end

if conf.group_id and not opts.skip_references_check then
Expand Down
14 changes: 13 additions & 1 deletion apisix/admin/credentials.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ local core = require("apisix.core")
local plugins = require("apisix.admin.plugins")
local plugin = require("apisix.plugin")
local resource = require("apisix.admin.resource")
local apisix_consumer = require("apisix.consumer")
local plugins_encrypt_conf = require("apisix.admin.plugins").encrypt_conf
local pairs = pairs

local function check_conf(_id, conf, _need_id, schema)
local function check_conf(id, conf, _need_id, schema, opts)
opts = opts or {}
local ok, err = core.schema.check(schema, conf)
if not ok then
return nil, {error_msg = "invalid configuration: " .. err}
Expand All @@ -42,6 +44,16 @@ local function check_conf(_id, conf, _need_id, schema)
return nil, {error_msg = "only supports auth type plugins in consumer credential"}
end
end

-- opts.sub_path is in the form of {consumer_name}/credentials or
-- {consumer_name}/credentials/{credential_id}
if not opts.skip_references_check and opts.sub_path then
local consumer_name = core.utils.split_uri(opts.sub_path)[1]
ok, err = apisix_consumer.check_duplicate_key(conf.plugins, consumer_name, id)
if not ok then
return nil, {error_msg = err}
end
end
end

return true, nil
Expand Down
7 changes: 4 additions & 3 deletions apisix/admin/resource.lua
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ local function check_forbidden_properties(conf, forbidden_properties)
end


function _M:check_conf(id, conf, need_id, typ, allow_time)
function _M:check_conf(id, conf, need_id, typ, allow_time, sub_path)
if self.name == "secrets" then
id = typ .. "/" .. id
end
Expand Down Expand Up @@ -123,7 +123,8 @@ function _M:check_conf(id, conf, need_id, typ, allow_time)
end

local conf_for_check = tbl_deepcopy(conf)
local ok, err = self.checker(id, conf_for_check, need_id, self.schema, {secret_type = typ})
local ok, err = self.checker(id, conf_for_check, need_id, self.schema,
{secret_type = typ, sub_path = sub_path})

if self.encrypt_conf then
self.encrypt_conf(id, conf)
Expand Down Expand Up @@ -253,7 +254,7 @@ function _M:put(id, conf, sub_path, args)
end

local need_id = not no_id_res[self.name]
local ok, err = self:check_conf(id, conf, need_id, typ)
local ok, err = self:check_conf(id, conf, need_id, typ, nil, sub_path)
if not ok then
return 400, err
end
Expand Down
108 changes: 108 additions & 0 deletions apisix/consumer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ local error = error
local ipairs = ipairs
local pairs = pairs
local type = type
local tostring = tostring
local string_sub = string.sub
local consumers

Expand Down Expand Up @@ -279,6 +280,113 @@ function _M.find_consumer(plugin_name, key, key_value)
end


-- The auth plugins below match the consumer by a unique key attribute, which is
-- the attribute that find_consumer() is called with at runtime. Duplicating such
-- keys across consumers/credentials makes the runtime matching ambiguous: the
-- last loaded consumer silently wins.
local plugin_unique_key_attrs = {
["key-auth"] = "key",
["basic-auth"] = "username",
["jwt-auth"] = "key",
["hmac-auth"] = "key_id",
}


local function get_auth_unique_keys(plugins_conf)
local keys
for plugin_name, plugin_conf in pairs(plugins_conf) do
local key_attr = plugin_unique_key_attrs[plugin_name]
if key_attr and type(plugin_conf) == "table" then
local key_value = plugin_conf[key_attr]
if type(key_value) == "string" then
if secret.is_secret_ref(key_value) then
core.log.info("skip duplicate check for the ", key_attr,
" of plugin ", plugin_name,
": secret reference cannot be resolved at write time")
else
keys = keys or {}
keys[plugin_name] = key_value
end
end
end
end

return keys
end


-- Reject the write when an auth plugin key in plugins_conf is already used by
-- another consumer or credential, since the runtime consumer matching can not
-- distinguish them.
--
-- The lookup goes through find_consumer(), i.e. the consumer data that this
-- process has already watched and synced from etcd -- the same view the
-- runtime uses for auth matching -- instead of pulling the full /consumers
-- range from etcd on every admin write. The tradeoff is that the local view
-- may lag the latest writes by one sync cycle, so a duplicate written moments
-- earlier can slip through under rapid successive or concurrent writes. This
-- is accepted: the check is best-effort protection against misconfiguration,
-- and the authoritative behavior at runtime remains last-loaded-wins.
--
-- When writing a consumer, consumer_name is its username and credential_id is
-- nil: a key owned by the consumer itself (inline or by its credentials) is
-- not treated as a duplicate. When writing a credential, both consumer_name
-- and credential_id are set: only the credential itself is skipped.
function _M.check_duplicate_key(plugins_conf, consumer_name, credential_id)
if not plugins_conf then
return true
end

-- the /consumers watcher may not be initialized yet, e.g. the write
-- arrives right after the worker starts: degrade to allow, consistent
-- with the best-effort nature of this check
if not consumers then
return true
end

local in_keys = get_auth_unique_keys(plugins_conf)
if not in_keys then
return true
end

-- the credential id may come from the request payload as a number, while
-- the cached credential_id is taken from the etcd key as a string
if credential_id then
credential_id = tostring(credential_id)
end

for plugin_name, key_value in pairs(in_keys) do
local key_attr = plugin_unique_key_attrs[plugin_name]
local owner = _M.find_consumer(plugin_name, key_attr, key_value)
if owner then
local is_self
if credential_id then
-- a credential does not conflict with itself
is_self = owner.consumer_name == consumer_name
and owner.credential_id == credential_id
else
-- a consumer does not conflict with itself or its own credentials
is_self = owner.consumer_name == consumer_name
end

if not is_self then
local owner_desc
if owner.credential_id then
owner_desc = "credential: " .. owner.credential_id ..
" of consumer: " .. owner.consumer_name
else
owner_desc = "consumer: " .. owner.consumer_name
end
return nil, "duplicate " .. key_attr .. " of plugin " .. plugin_name
.. " found with " .. owner_desc
end
end
end

return true
end


local function check_consumer(consumer, key)
local data_valid
local err
Expand Down
Loading
Loading