Files
op-packages/luci-lib-linkeaseauth/tests/test_linkease_auth.lua
T

178 lines
6.1 KiB
Lua

local source_root = assert(os.getenv("LINKEASE_AUTH_SOURCE_ROOT"), "LINKEASE_AUTH_SOURCE_ROOT is required")
package.path = source_root .. "/?.lua;" .. package.path
local function equal(actual, expected, message)
if actual ~= expected then
error((message or "values differ") .. ": got " .. tostring(actual) .. ", want " .. tostring(expected), 2)
end
end
local function has_header(headers, pattern)
for _, header in ipairs(headers) do
if header[1] == "Set-Cookie" and header[2]:match(pattern) then return true end
end
return false
end
local function scenario(options)
options = options or {}
local headers = {}
local result = {}
local cookies = options.cookies or {}
local environment = options.environment or { HTTP_HOST = "192.168.30.7:10000" }
local forms = options.forms or {}
package.loaded["luci.model.linkease.auth"] = nil
local auth = require "luci.model.linkease.auth"
local bridge = auth.new({
http = {
getcookie = function(name) return cookies[name] end,
getenv = function(name) return environment[name] end,
formvalue = function(name) return forms[name] end,
header = function(name, value) headers[#headers + 1] = { name, value } end,
redirect = function(value) result.redirect = value end,
status = function(code, message) result.status = code; result.message = message end
},
ubus = function(object, method, request)
if object == "session" and method == "get" and request.ubus_rpc_session == options.valid_sid then
return { values = { username = "root" } }
end
return nil
end,
lan_ip = function() return "192.168.30.7" end,
build_url = function(...)
local arguments = {...}
local url = "/cgi-bin/luci/admin/services/linkease_auth/auth_finish"
if arguments[5] then url = url .. "/" .. arguments[5] end
return url
end
})
return bridge, result, headers
end
local bridge, result, headers = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "/apps/dockermanager/" }
})
bridge:auth()
equal(result.redirect, "/apps/dockermanager/")
equal(has_header(headers, "linkease_openwrt_sid=valid%-session"), true)
equal(has_header(headers, "linkease_openwrt_sid=.*Path=/apps"), true)
bridge, result, headers = scenario({ forms = { ["return"] = "/apps/kaiplus/" } })
bridge:auth()
local state = result.redirect:match("/auth_finish/([A-Za-z0-9_-]+)$")
equal(type(state), "string", "auth begin must encode return intent into the protected path")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session"
})
bridge:auth_finish(state)
equal(result.redirect, "/apps/kaiplus/")
local first_bridge, first_result = scenario({
forms = { ["return"] = "/apps/dockermanager/containers/abc?tab=logs&follow=1" }
})
first_bridge:auth()
local first_state = first_result.redirect:match("/auth_finish/([A-Za-z0-9_-]+)$")
equal(type(first_state), "string", "deep link state must be encoded")
local second_bridge, second_result = scenario({
forms = { ["return"] = "/apps/kaiplus/?conversation=second" }
})
second_bridge:auth()
local second_state = second_result.redirect:match("/auth_finish/([A-Za-z0-9_-]+)$")
equal(type(second_state), "string", "second tab state must be encoded")
bridge, result = scenario({
cookies = { sysauth = "valid-session", linkease_openwrt_pending_return = "%2Fapps%2Fwrong%2F" },
valid_sid = "valid-session"
})
bridge:auth_finish(first_state)
equal(result.redirect, "/apps/dockermanager/containers/abc?tab=logs&follow=1", "first tab must use its own path state")
bridge, result = scenario({
cookies = { sysauth = "valid-session", linkease_openwrt_pending_return = "%2Fapps%2Fwrong%2F" },
valid_sid = "valid-session"
})
bridge:auth_finish(second_state)
equal(result.redirect, "/apps/kaiplus/?conversation=second", "second tab must use its own path state")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session"
})
bridge:auth_finish("not_base64url_@@")
equal(result.redirect, "/apps/", "malformed state must fail closed")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session"
})
bridge:auth_finish(string.rep("A", 4097))
equal(result.redirect, "/apps/", "oversized state must fail closed")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "http://192.168.30.7:8192/apps/dockermanager/" }
})
bridge:auth()
equal(result.redirect, "http://192.168.30.7:8192/apps/dockermanager/")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "http://192.168.30.7:3200/" }
})
bridge:auth()
equal(result.redirect, "http://192.168.30.7:3200/", "same-device external app port must be allowed")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "http://192.168.30.7:10000/cgi-bin/luci/" }
})
bridge:auth()
equal(result.redirect, "/apps/", "same-authority non-app path must remain rejected")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "http://evil.example/apps/dockermanager/" }
})
bridge:auth()
equal(result.redirect, "/apps/")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "/apps/dockermanager/%0d%0aLocation:%20http://evil.example/" }
})
bridge:auth()
equal(result.redirect, "/apps/dockermanager/%0d%0aLocation:%20http://evil.example/")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "/apps/dockermanager/\r\nLocation: http://evil.example/" }
})
bridge:auth()
equal(result.redirect, "/apps/", "raw control characters must be rejected")
bridge, result = scenario({
cookies = { sysauth = "valid-session" },
valid_sid = "valid-session",
forms = { ["return"] = "http://192.168.30.7:8192@evil.example/apps/dockermanager/" }
})
bridge:auth()
equal(result.redirect, "/apps/")
bridge, result = scenario()
bridge:auth_finish()
equal(result.status, 403)
print("linkease auth bridge tests passed")