luci-base: dispatcher: rework dispatching and menu filtering logic

- Prefer nodes that do not require authentication over nodes that do
 - Honour ACL dependencies while resolving firstchild nodes
 - Consider currently active session while scanning menu tree instead
   of only loading effective ACLs when a login node is encountered
 - Do not consider nodes for firstchild dispatching which specify a
   special "firstchild_ineligible" property
 - Hide menu nodes that have no accessible children

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
Jo-Philipp Wich 2021-07-28 09:00:16 +02:00
parent ea9b5e87e6
commit e4d24f07c9
3 changed files with 184 additions and 156 deletions

View file

@ -3021,7 +3021,7 @@ function scrubMenu(node) {
for (var k in node.children) { for (var k in node.children) {
var child = scrubMenu(node.children[k]); var child = scrubMenu(node.children[k]);
if (child.title) if (child.title && !child.firstchild_ineligible)
hasSatisfiedChild = hasSatisfiedChild || child.satisfied; hasSatisfiedChild = hasSatisfiedChild || child.satisfied;
} }
} }

View file

@ -587,64 +587,6 @@ local function check_authentication(method)
return session_retrieve(sid) return session_retrieve(sid)
end end
local function get_children(node)
local children = {}
if not node.wildcard and type(node.children) == "table" then
for name, child in pairs(node.children) do
children[#children+1] = {
name = name,
node = child,
order = child.order or 1000
}
end
table.sort(children, function(a, b)
if a.order == b.order then
return a.name < b.name
else
return a.order < b.order
end
end)
end
return children
end
local function find_subnode(root, prefix, recurse, descended)
local children = get_children(root)
if #children > 0 and (not descended or recurse) then
local sub_path = { unpack(prefix) }
if recurse == false then
recurse = nil
end
for _, child in ipairs(children) do
sub_path[#prefix+1] = child.name
local res_path = find_subnode(child.node, sub_path, recurse, true)
if res_path then
return res_path
end
end
end
if descended then
if not recurse or
root.action.type == "cbi" or
root.action.type == "form" or
root.action.type == "view" or
root.action.type == "template" or
root.action.type == "arcombine"
then
return prefix
end
end
end
local function merge_trees(node_a, node_b) local function merge_trees(node_a, node_b)
for k, v in pairs(node_b) do for k, v in pairs(node_b) do
if k == "children" then if k == "children" then
@ -786,85 +728,177 @@ local function init_template_engine(ctx)
return tpl return tpl
end end
function dispatch(request) local function is_authenticated(auth)
--context._disable_memtrace = require "luci.debug".trap_memtrace("l")
local ctx = context
local auth, cors, suid, sgid
local menu = menu_json()
local page = menu
local requested_path_full = {}
local requested_path_node = {}
local requested_path_args = {}
local required_path_acls = {}
for i, s in ipairs(request) do
if type(page.children) ~= "table" or not page.children[s] then
page = nil
break
end
if not page.children[s].satisfied then
page = nil
break
end
page = page.children[s]
auth = page.auth or auth
cors = page.cors or cors
suid = page.setuser or suid
sgid = page.setgroup or sgid
if type(page.depends) == "table" and type(page.depends.acl) == "table" then
for _, group in ipairs(page.depends.acl) do
local found = false
for _, item in ipairs(required_path_acls) do
if item == group then
found = true
break
end
end
if not found then
required_path_acls[#required_path_acls + 1] = group
end
end
end
requested_path_full[i] = s
requested_path_node[i] = s
if page.wildcard then
for j = i + 1, #request do
requested_path_args[j - i] = request[j]
requested_path_full[j] = request[j]
end
break
end
end
local tpl = init_template_engine(ctx)
ctx.args = requested_path_args
ctx.path = requested_path_node
ctx.dispatched = page
ctx.requestpath = ctx.requestpath or requested_path_full
ctx.requestargs = ctx.requestargs or requested_path_args
ctx.requested = ctx.requested or page
if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
local sid, sdat, sacl local sid, sdat, sacl
for _, method in ipairs(auth.methods) do for _, method in ipairs(auth.methods) do
sid, sdat, sacl = check_authentication(method) sid, sdat, sacl = check_authentication(method)
if sid and sdat and sacl then if sid and sdat and sacl then
return sid, sdat, sacl
end
end
end
end
local function ctx_append(ctx, name, node)
ctx.path = ctx.path or {}
ctx.path[#ctx.path + 1] = name
ctx.acls = ctx.acls or {}
local acls = (type(node.depends) == "table" and type(node.depends.acl) == "table") and node.depends.acl or {}
for _, acl in ipairs(acls) do
ctx.acls[_] = acl
end
ctx.auth = node.auth or ctx.auth
ctx.cors = node.cors or ctx.cors
ctx.suid = node.setuser or ctx.suid
ctx.sgid = node.setgroup or ctx.sgid
return ctx
end
local function node_weight(node)
local weight = node.order or 9999
if weight > 9999 then
weight = 9999
end
if type(node.auth) == "table" and node.auth.login then
weight = weight + 10000
end
return weight
end
local function resolve_firstchild(node, sacl, login_allowed, ctx)
local candidate = nil
local candidate_ctx = nil
for name, child in pairs(node.children) do
if child.satisfied then
if not sacl then
local _
_, _, sacl = is_authenticated(node.auth)
end
local cacl = (type(child.depends) == "table") and child.depends.acl or nil
local login = login_allowed or (type(child.auth) == "table" and child.auth.login)
if login or check_acl_depends(cacl, sacl and sacl["access-group"]) ~= nil then
if child.title and type(child.action) == "table" then
local child_ctx = ctx_append(util.clone(ctx, true), name, child)
if child.action.type == "firstchild" then
if not candidate or node_weight(candidate) > node_weight(child) then
local have_grandchild = resolve_firstchild(child, sacl, login, child_ctx)
if have_grandchild then
candidate = child
candidate_ctx = child_ctx
end
end
elseif not child.firstchild_ineligible then
if not candidate or node_weight(candidate) > node_weight(child) then
candidate = child
candidate_ctx = child_ctx
end
end
end
end
end
end
if candidate then
for k, v in pairs(candidate_ctx) do
ctx[k] = v
end
return true
end
return false
end
local function resolve_page(tree, request_path)
local node = tree
local sacl = nil
local login = false
local ctx = {}
for i, s in ipairs(request_path) do
node = node.children and node.children[s]
if not node or not node.satisfied then
break
end
ctx_append(ctx, s, node)
if not sacl then
local _
_, _, sacl = is_authenticated(node.auth)
end
if not login and type(node.auth) == "table" and node.auth.login then
login = true
end
if node.wildcard then
ctx.request_args = {}
ctx.request_path = util.clone(ctx.path, true)
for j = i + 1, #request_path do
ctx.request_path[j] = request_path[j]
ctx.request_args[j - i] = request_path[j]
end
break break
end end
end end
if not (sid and sdat and sacl) and auth.login then if node and type(node.action) == "table" and node.action.type == "firstchild" then
resolve_firstchild(node, sacl, login, ctx)
end
ctx.acls = ctx.acls or {}
ctx.path = ctx.path or {}
ctx.request_args = ctx.request_args or {}
ctx.request_path = ctx.request_path or util.clone(request_path, true)
node = tree
for _, s in ipairs(ctx.path or {}) do
node = node.children[s]
assert(node, "Internal node resolve error")
end
return node, ctx
end
function dispatch(request)
--context._disable_memtrace = require "luci.debug".trap_memtrace("l")
local ctx = context
local auth, cors, suid, sgid
local menu = menu_json()
local page, lookup_ctx = resolve_page(menu, request)
local action = (page and type(page.action) == "table") and page.action or {}
local tpl = init_template_engine(ctx)
ctx.args = lookup_ctx.request_args
ctx.path = lookup_ctx.path
ctx.dispatched = page
ctx.requestpath = ctx.requestpath or lookup_ctx.request_path
ctx.requestargs = ctx.requestargs or lookup_ctx.request_args
ctx.requested = ctx.requested or page
if type(lookup_ctx.auth) == "table" and next(lookup_ctx.auth) then
local sid, sdat, sacl = is_authenticated(lookup_ctx.auth)
if not (sid and sdat and sacl) and lookup_ctx.auth.login then
local user = http.getenv("HTTP_AUTH_USER") local user = http.getenv("HTTP_AUTH_USER")
local pass = http.getenv("HTTP_AUTH_PASS") local pass = http.getenv("HTTP_AUTH_PASS")
@ -911,8 +945,8 @@ function dispatch(request)
ctx.authacl = sacl ctx.authacl = sacl
end end
if #required_path_acls > 0 then if #lookup_ctx.acls > 0 then
local perm = check_acl_depends(required_path_acls, ctx.authacl and ctx.authacl["access-group"]) local perm = check_acl_depends(lookup_ctx.acls, ctx.authacl and ctx.authacl["access-group"])
if perm == nil then if perm == nil then
http.status(403, "Forbidden") http.status(403, "Forbidden")
return return
@ -923,13 +957,11 @@ function dispatch(request)
end end
end end
local action = (page and type(page.action) == "table") and page.action or {}
if action.type == "arcombine" then if action.type == "arcombine" then
action = (#requested_path_args > 0) and action.targets[2] or action.targets[1] action = (#lookup_ctx.request_args > 0) and action.targets[2] or action.targets[1]
end end
if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then if lookup_ctx.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
luci.http.status(200, "OK") luci.http.status(200, "OK")
luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*") luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
@ -942,12 +974,12 @@ function dispatch(request)
end end
end end
if sgid then if lookup_ctx.sgid then
sys.process.setgroup(sgid) sys.process.setgroup(lookup_ctx.sgid)
end end
if suid then if lookup_ctx.suid then
sys.process.setuser(suid) sys.process.setuser(lookup_ctx.suid)
end end
if action.type == "view" then if action.type == "view" then
@ -970,7 +1002,7 @@ function dispatch(request)
'of type "' .. type(func) .. '".') 'of type "' .. type(func) .. '".')
local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {} local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
for _, s in ipairs(requested_path_args) do for _, s in ipairs(lookup_ctx.request_args) do
argv[#argv + 1] = s argv[#argv + 1] = s
end end
@ -979,13 +1011,8 @@ function dispatch(request)
error500(err) error500(err)
end end
elseif action.type == "firstchild" then --elseif action.type == "firstchild" then
local sub_request = find_subnode(page, requested_path_full, action.recurse) -- tpl.render("empty_node_placeholder", getfenv(1))
if sub_request then
dispatch(sub_request)
else
tpl.render("empty_node_placeholder", getfenv(1))
end
elseif action.type == "alias" then elseif action.type == "alias" then
local sub_request = {} local sub_request = {}
@ -993,7 +1020,7 @@ function dispatch(request)
sub_request[#sub_request + 1] = name sub_request[#sub_request + 1] = name
end end
for _, s in ipairs(requested_path_args) do for _, s in ipairs(lookup_ctx.request_args) do
sub_request[#sub_request + 1] = s sub_request[#sub_request + 1] = s
end end
@ -1011,7 +1038,7 @@ function dispatch(request)
n = n + 1 n = n + 1
end end
for _, s in ipairs(requested_path_args) do for _, s in ipairs(lookup_ctx.request_args) do
sub_request[#sub_request + 1] = s sub_request[#sub_request + 1] = s
end end
@ -1021,19 +1048,18 @@ function dispatch(request)
tpl.render(action.path, getfenv(1)) tpl.render(action.path, getfenv(1))
elseif action.type == "cbi" then elseif action.type == "cbi" then
_cbi({ config = action.config, model = action.path }, unpack(requested_path_args)) _cbi({ config = action.config, model = action.path }, unpack(lookup_ctx.request_args))
elseif action.type == "form" then elseif action.type == "form" then
_form({ model = action.path }, unpack(requested_path_args)) _form({ model = action.path }, unpack(lookup_ctx.request_args))
else else
local root = find_subnode(menu, {}, true) if not menu.children then
if not root then
error404("No root node was registered, this usually happens if no module was installed.\n" .. error404("No root node was registered, this usually happens if no module was installed.\n" ..
"Install luci-mod-admin-full and retry. " .. "Install luci-mod-admin-full and retry. " ..
"If the module is already installed, try removing the /tmp/luci-indexcache file.") "If the module is already installed, try removing the /tmp/luci-indexcache file.")
else else
error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" .. error404("No page is registered at '/" .. table.concat(lookup_ctx.request_path, "/") .. "'.\n" ..
"If this url belongs to an extension, make sure it is properly installed.\n" .. "If this url belongs to an extension, make sure it is properly installed.\n" ..
"If the extension was recently installed, try removing the /tmp/luci-indexcache file.") "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
end end
@ -1135,7 +1161,8 @@ function createtree_json()
setgroup = "string", setgroup = "string",
setuser = "string", setuser = "string",
title = "string", title = "string",
wildcard = "boolean" wildcard = "boolean",
firstchild_ineligible = "boolean"
} }
local files = {} local files = {}

View file

@ -87,7 +87,8 @@
}, },
"depends": { "depends": {
"acl": [ "luci-base" ] "acl": [ "luci-base" ]
} },
"firstchild_ineligible": true
}, },
"admin/uci": { "admin/uci": {