Skip to content

Commit

Permalink
feat(rbac): added role_source to update rbac user's rbac role while…
Browse files Browse the repository at this point in the history
… the IDP group mapping changes. (#8187)

* feat(oidc): adds `role_source` to support update RBAC user's rbac role while the IDP group mapping was changed.

---------

Co-authored-by: Makito <[email protected]>
  • Loading branch information
raoxiaoyan and sumimakito authored Apr 23, 2024
1 parent acbc244 commit 4ce30a5
Show file tree
Hide file tree
Showing 13 changed files with 553 additions and 213 deletions.
2 changes: 2 additions & 0 deletions changelog/unreleased/kong-ee/update_rbac_role_idp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
message: When authenticating Kong Manager with IDPs (e.g., OIDC, LDAP), the source of an RBAC role will be stored in its `role_source` field, which enables the existing roles with a source of `idp` to be removed upon new logins after IDP role mapping has changed. This also allows users to change a role's source between `local` and `idp` via the Admin API manually.
type: feature
34 changes: 34 additions & 0 deletions kong/api/routes/admins.lua
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,40 @@ return {
return kong.response.exit(201, { roles = roles })
end,

PATCH = function(self, db, helpers, parent)
-- we have the user, now verify our roles
if not self.params.roles or type(self.params.roles) == "userdata" then
return kong.response.exit(400, { message = "must provide at least one role or not null" })
end

local roles, err = rbac.objects_from_names(db, self.params.roles, "role")
if err then
if err:find("could not find role with name " .. tostring(self.params.roles), nil, true) then
return kong.response.exit(400, { message = err })
else
return endpoints.handle_error(err)
end
end

for _, role in ipairs(roles or {}) do
local _, err_t = db.rbac_user_roles:update({
user = { id = self.admin.rbac_user.id },
role = { id = role.id },
}, {
role_source = self.params.role_source,
})

if err_t then
return endpoints.handle_error(err_t)
end
end

local cache_key = db.rbac_user_roles:cache_key(self.admin.rbac_user.id)
kong.cache:invalidate(cache_key)

return kong.response.exit(201, { roles = roles })
end,

DELETE = function(self, db, helpers, parent)
-- we have the user, now verify our roles
if not self.params.roles then
Expand Down
32 changes: 26 additions & 6 deletions kong/api/routes/kong.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ local strip_foreign_schemas = function(fields)
end
end

local function retrieve_ws_name(self, ws_id)
local res = {}
for _, ws in ipairs(self.workspaces) do
if ws.id == ws_id then
res[#res + 1] = { name = ws.name, id = ws_id }
end
end

return res and res[1]
end

local function ws_and_rbac_helper(self)
self.workspaces = {}
local admin_auth = kong.configuration.admin_gui_auth
Expand Down Expand Up @@ -101,14 +112,9 @@ local function ws_and_rbac_helper(self)
kong.cache:invalidate(cache_key)

-- get roles across all workspaces (except for the wildcard "*" one, the 3rd argument)
local wss, roles = rbac.find_all_ws_for_rbac_user(ngx.ctx.rbac.user, ngx.null, false)
local wss, roles = rbac.find_all_ws_for_rbac_user(ngx.ctx.rbac.user, null, false)
self.workspaces = wss

if err then
log(ERR, "[userinfo] ", err)
return kong.response.exit(500, err)
end

local rbac_enabled = kong.configuration.rbac
if rbac_enabled == "on" or rbac_enabled == "both" then
self.permissions.endpoints = rbac.readable_endpoints_permissions(roles)
Expand All @@ -117,6 +123,19 @@ local function ws_and_rbac_helper(self)
if rbac_enabled == "entity" or rbac_enabled == "both" then
self.permissions.entities = rbac.readable_entities_permissions(roles)
end

local user_roles = rbac.get_user_roles(kong.db, ngx.ctx.rbac.user, null)

local belong_roles = {}
for _, role in ipairs(user_roles or {}) do
if not role.is_default then
belong_roles[#belong_roles + 1] = {
name = role.name,
workspace = retrieve_ws_name(self, role.ws_id),
}
end
end
self.roles = belong_roles
end


Expand Down Expand Up @@ -279,6 +298,7 @@ return {
return kong.response.exit(200, {
admin = admins.transmogrify(self.admin),
groups = self.groups,
roles = self.roles,
permissions = self.permissions,
workspaces = self.workspaces,
session = {
Expand Down
1 change: 1 addition & 0 deletions kong/db/schema/entities/rbac_user_roles.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ return {
fields = {
{ user = { description = "The RBAC user associated with the role.", type = "foreign", required = true, reference = "rbac_users", on_delete = "cascade" } },
{ role = { description = "The RBAC role assigned to the user.", type = "foreign", required = true, reference = "rbac_roles", on_delete = "cascade" } },
{ role_source = { description = "The origin of the RBAC user role.", type = "string", default = "local", one_of = { "local", "idp" }, indexed = true } }
}
}
5 changes: 5 additions & 0 deletions kong/enterprise_edition/admins_helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ local function transmogrify(admin)
status = admin.status,
rbac_token_enabled = admin.rbac_token_enabled,
belong_workspace = admin.belong_workspace,
groups = admin.groups,
workspaces = admin.workspaces,
created_at = admin.created_at,
updated_at = admin.updated_at,
Expand Down Expand Up @@ -590,6 +591,10 @@ function _M.find_by_username_or_id(username_or_id, raw, require_workspace_ctx)
local rbac_user = kong.db.rbac_users:select(admin.rbac_user, { workspace = null, show_ws_id = true })

local wss, _, err = rbac.find_all_ws_for_rbac_user(rbac_user, null, true)

admin.workspaces = wss
admin.groups = rbac.get_user_groups(kong.db, rbac_user)

if err then
return nil, err
end
Expand Down
5 changes: 4 additions & 1 deletion kong/enterprise_edition/api_helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ local errors = require "kong.db.errors"
local entity = require "kong.db.schema.entity"
local ws_schema = require "kong.db.schema.entities.workspaces"
local get_request_id = require("kong.tracing.request_id").get
local tablex = require "pl.tablex"

local fmt = string.format
local kong = kong
Expand Down Expand Up @@ -327,7 +328,9 @@ function _M.authenticate(self, rbac_enabled, gui_auth)
end

self.rbac_user = rbac_user
self.groups = ctx.authenticated_groups
local groups = rbac.get_user_groups(kong.db, rbac_user)

self.groups = tablex.map(function(group) return { name = group.name } end, groups)
self.admin = admin
-- set back workspace context from request
ctx.workspace = old_ws
Expand Down
86 changes: 45 additions & 41 deletions kong/enterprise_edition/auth_plugin_helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,13 @@ function _M.delete_admin_groups_or_roles(admin)
kong.db.rbac_user_groups:delete(group)
end

-- FIXME: should delete roles from IdPs only
-- delete rbac_user_roles
-- local cache_key = kong.db.rbac_user_roles:cache_key(admin.rbac_user.id)
-- kong.cache:invalidate(cache_key)
-- only delete rbac_user_roles from idp
cache_key = kong.db.rbac_user_roles:cache_key(admin.rbac_user.id)
kong.cache:invalidate(cache_key)

-- for rbac_user_role, _ in kong.db.rbac_user_roles:each_for_user({ id = admin.rbac_user.id }) do
-- kong.db.rbac_user_roles:delete(rbac_user_role)
-- end
for rbac_user_role, _ in kong.db.rbac_user_roles:each_for_user({ id = admin.rbac_user.id }, nil, { search_fields = { role_source = { eq = "idp" } } }) do
kong.db.rbac_user_roles:delete(rbac_user_role)
end

end

Expand Down Expand Up @@ -184,53 +183,58 @@ function _M.map_admin_roles_by_idp_claim(admin, claim_values)
end

local existing_roles, _ = rbac.get_user_roles(kong.db, admin.rbac_user, ngx.null)
-- assign roles to admin by each ws
for ws_id, ws_roles in pairs(roles_by_ws) do
-- Todo: rbac.set_user_role improvment.
-- the rbac.set_user_role requires ws_id when insert new roles,
-- but not checking ws when deleting the exist roles. So that,
-- we always input all the user's roles here.
local _, err_str = rbac.set_user_roles(kong.db, admin.rbac_user, ws_roles, ws_id)
if err_str then
ngx.log(ngx.NOTICE, err_str)
local retrieve_ws_roles = function(ws_id)
local ws_roles = {}
for _, role in pairs(existing_roles or {}) do
if not role.is_default and role.ws_id == ws_id then
ws_roles[role.name] = role
end
end
return ws_roles
end
-- assign roles to admin by each ws
local user_pk = { id = admin.rbac_user.id }

-- delete roles that are not in the claim
local check_role_exists = function(ws_id, role)
local ws_roles = roles_by_ws[ws_id]
for ws_id, role_names in pairs(roles_by_ws) do
local ws_exists_roles = retrieve_ws_roles(ws_id)

if not ws_roles then
return false
end
for _, role_name in ipairs(role_names) do
local exist_role = ws_exists_roles[role_name]

local exists = false
for _, role_name in ipairs(ws_roles) do
if role_name == role.name then
exists = true
break
end
end
return exists
end
if not exist_role then
local role = kong.db.rbac_roles:select_by_name(role_name, { workspace = ws_id })

if role then
local _, err = kong.db.rbac_user_roles:insert({
user = user_pk,
role = { id = role.id },
role_source = "idp"
})

for i = 1, #existing_roles do
local role = existing_roles[i]
if not role.is_default then
local ws_id = role.ws_id
if err then
kong.log.err("Failure insert the role ", role_name, ". Error message:", err)
end

if not check_role_exists(ws_id, role) then
local ok, err = kong.db.rbac_user_roles:delete({
user = { id = admin.rbac_user.id },
role = { id = role.id },
else
kong.log.warn(string.format("role '%s' does not exist of the workspace '%s'", role_name, ws_id))
end

else
local _, err = kong.db.rbac_user_roles:update({
user = user_pk,
role = { id = exist_role.id },
}, {
role_source = "idp",
})
if not ok then
kong.log.err("Error while deleting role: " .. err .. ".")
if err then
kong.log.err("Failure update the role source of the role ", role_name, ". Error message:", err)
end
end
end
end

local cache_key = kong.db.rbac_user_roles:cache_key(admin.rbac_user.id)
kong.cache:invalidate(cache_key)
end

-- [[ OpenID Connect helpers (ONLY for Admin API usages with Kong Manager for now)
Expand Down
34 changes: 31 additions & 3 deletions kong/rbac/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ local function get_user_roles(db, user, workspace)
end

if relationship and (workspace == null or workspace == relationship.ws_id) then
relationship.role_source = relationship_ids[i].role_source
relationship_objs[#relationship_objs + 1] = relationship
end
end
Expand All @@ -569,6 +570,23 @@ local function retrieve_user_groups(rbac_user)
return rbac_user_groups
end

function _M.get_user_groups(db, rbac_user)
local cache_key = db.rbac_user_groups:cache_key(rbac_user.id)
local rbac_user_groups = kong.cache:get(cache_key, nil, retrieve_user_groups, rbac_user)

local groups = {}
for _, rbac_user_group in ipairs(rbac_user_groups or {}) do
local group = rbac_user_group and rbac_user_group.group

group = select_from_cache(db.groups, group.id, retrieve_group)
if group then
table.insert(groups, group)
end
end

return groups
end

function _M.get_groups_roles(db, rbac_user, workspace)
assert(workspace == null or (type(workspace) == "string" and workspace ~= "*"),
"workspace must be an id (string uuid) or ngx.null to mean global")
Expand Down Expand Up @@ -1702,6 +1720,15 @@ local NOT_PERMIT_ROUTE = {}

NOT_PERMIT_ROUTE["/admins/:admin/roles"] = {
handler = function(request)
-- doesn't support update the role_source while admin_gui_auth is not openid-connect or ldap-auth-advanced
local method = request.req.method
local admin_gui_auth = kong.configuration.admin_gui_auth

if not (admin_gui_auth == "openid-connect" or admin_gui_auth == "ldap-auth-advanced")
and method == "PATCH" then
return true, 405, "Method Not Allowed"
end

local rbac_user = ngx.ctx.rbac.user
local name_or_id = request.params.admin
local admin = find_admin_by_username_or_id(name_or_id)
Expand All @@ -1711,7 +1738,7 @@ NOT_PERMIT_ROUTE["/admins/:admin/roles"] = {

return false
end,
methods = { POST = true, DELETE = true },
methods = { POST = true, PATCH = true, DELETE = true },
err = "the admin should not update their own roles",
}

Expand Down Expand Up @@ -1743,8 +1770,9 @@ function _M.validate_permit_update(request)
local route = NOT_PERMIT_ROUTE[route_name]
if route then
local method = request.req.method
if route.methods[method] and route.handler(request) then
return kong.response.exit(403, { message = route.err })
local ok, status, err = route.handler(request)
if route.methods[method] and ok then
return kong.response.exit(status or 403, { message = err or route.err })
end
end
end
Expand Down
Loading

0 comments on commit 4ce30a5

Please sign in to comment.