DVX_GUI/server/tools/review.lua
2026-10-15 17:45:45 -05:00

196 lines
7.5 KiB
Lua

-- review.lua -- the operator's review of uploaded packages, run on the operator's own computer: the
-- one place the repository's secret key is kept. It lists what waits, fetches a version and shows
-- what it is (the package tool's own check of it), approves a version with the repository's
-- signature made here, or rejects it with a note its uploader reads. The service checks the
-- signature against the repository's public key; the secret key never leaves this computer.
--
-- calog tools/review.lua -- review.conf <command> [arguments] (in the server folder)
--
-- It needs singemaster's lib/config.lua (singe=DIR when singemaster is not at ../../singemaster),
-- and the package tool from a DVX build or the SDK.
local USAGE = [[
usage: calog tools/review.lua -- <review.conf> <command> [arguments]
pending versions waiting for a review, oldest first
fetch <id> <version> download one into the work folder and show what it is
approve <id> <version> [note] sign what was fetched with the repository's key and approve it
reject <id> <version> <note> reject one; the note is what the uploader reads
withdraw <id> stop listing and handing out a package
publish <id> list it again
review.conf:
server = https://packages.example.com the service, as anyone reaches it
token = ... the service's adminToken, or an administrator's session
dvxpkg = /path/to/dvxpkg the package tool
secret = /path/to/KPUNCH.SEC the repository's secret key
work = review where fetched packages and their signatures are kept
]]
local REQUIRED = { "server", "token", "dvxpkg", "secret", "work" }
-- calogArgs(): the configuration, the command and its arguments, and singe=DIR if given; without
-- it, singemaster is beside this repository (from this file: tools, server, the DVX tree)
local plain = {}
local SINGE = (calogScript():match("^(.*)[/\\]") or ".") .. "/../../../singemaster"
for _, value in ipairs(calogArgs()) do
local dir = value:match("^singe=(.+)$")
if dir then
SINGE = dir
else
plain[#plain + 1] = value
end
end
if #plain < 2 then
calogPrint(USAGE)
calogExit(2)
end
local config = load(fsRead(SINGE .. "/lib/config.lua"), "@" .. SINGE .. "/lib/config.lua")()
local settings, configError = config.load(plain[1], REQUIRED, {})
if not settings then
calogPrint("configuration: " .. tostring(configError))
calogExit(1)
end
local BASE = settings.server:gsub("/+$", "")
local command = plain[2]
-- The service's answer: its status, the JSON it sent (or {}), and the whole reply
local function call(method, path, body)
local reply = httpRequest({ method = method, url = BASE .. path, body = body and jsonStringify(body) or nil,
headers = { ["Content-Type"] = "application/json", Authorization = "Bearer " .. settings.token } })
local parsed = {}
if reply.body and reply.body ~= "" then
local ok, value = pcall(jsonParse, reply.body)
if ok and type(value) == "table" then
parsed = value
end
end
return reply.status, parsed, reply
end
-- Stop with what the service said, when it did not say yes
local function expect(status, parsed, want)
if status ~= want then
calogPrint(string.format("the service answered %s: %s", tostring(status), tostring(parsed.message or parsed.error or "")))
calogExit(1)
end
end
-- Where a fetched version, and the signature made of it, are kept
local function localPath(id, version, extension)
return string.format("%s/%s-%s.%s", settings.work, id, version, extension)
end
-- The package tool, its exit status and what it printed
local function dvxpkg(args)
local argv = { settings.dvxpkg }
for _, value in ipairs(args) do
argv[#argv + 1] = value
end
local run = procRun(argv)
return run.exit, (run.stdout or "") .. (run.stderr or "")
end
-- Download a version into the work folder, check its bytes against the digest the service sent, and
-- show its manifest and files as the package tool reads them. Stops if the tool refuses it.
local function fetch(id, version)
local status, parsed, reply = call("GET", "/v1/admin/download/" .. id .. "/" .. version)
expect(status, parsed, 200)
if cryptoHashSha256(reply.body) ~= reply.headers["x-content-sha256"] then
calogPrint("the download does not match the digest the service sent; try again")
calogExit(1)
end
fsMkdir(settings.work)
local path = localPath(id, version, "DPK")
fsWrite(path, reply.body)
local checked, manifest = dvxpkg({ "check", path })
if checked ~= 0 then
calogPrint("the package tool refuses " .. path .. ":")
calogPrint(manifest)
calogExit(1)
end
local _, info = dvxpkg({ "info", path })
calogPrint(info)
calogPrint(string.format("fetched %s (%d bytes, sha256 %s)", path, #reply.body, reply.headers["x-content-sha256"]))
return path
end
local function pending()
local status, parsed = call("GET", "/v1/admin/pending")
expect(status, parsed, 200)
if (parsed.count or 0) == 0 then
calogPrint("nothing is waiting")
return
end
for _, row in ipairs(parsed.pending) do
calogPrint(string.format("%s %s (%s, %d bytes) from %s, %s", row.packageId, row.version, tostring(row.kind),
row.size or 0, tostring(row.uploader or "the operator"), tostring(row.uploadedAt)))
calogPrint(string.format(" %s%s", tostring(row.name), row.summary and (": " .. row.summary) or ""))
calogPrint(string.format(" publisher's key %s", tostring(row.signerKey or "none")))
end
end
-- Approve what was reviewed: the fetched file (fetched now if it was not), signed here
local function approve(id, version, note)
local path = localPath(id, version, "DPK")
if not fsExists(path) then
fetch(id, version)
end
local signed, said = dvxpkg({ "sign", path, settings.secret, "-d" })
if signed ~= 0 then
calogPrint("the package tool could not sign it:")
calogPrint(said)
calogExit(1)
end
local status, parsed = call("POST", "/v1/admin/version/state",
{ package = id, version = version, state = "approved", signature = fsRead(localPath(id, version, "SIG")), note = note })
expect(status, parsed, 200)
calogPrint(string.format("approved %s %s", id, version))
end
local function reject(id, version, note)
local status, parsed = call("POST", "/v1/admin/version/state", { package = id, version = version, state = "rejected", note = note })
expect(status, parsed, 200)
calogPrint(string.format("rejected %s %s", id, version))
end
local function packageState(id, state)
local status, parsed = call("POST", "/v1/admin/package/state", { package = id, state = state })
expect(status, parsed, 200)
calogPrint(string.format("%s is %s", id, state))
end
local COMMANDS = {
pending = { 0, function() pending() end },
fetch = { 2, function(a) fetch(a[1], a[2]) end },
approve = { 2, function(a) approve(a[1], a[2], a[3]) end },
reject = { 3, function(a) reject(a[1], a[2], a[3]) end },
withdraw = { 1, function(a) packageState(a[1], "withdrawn") end },
publish = { 1, function(a) packageState(a[1], "published") end },
}
local entry = COMMANDS[command]
local arguments = { table.unpack(plain, 3) }
if not entry or #arguments < entry[1] then
calogPrint(USAGE)
calogExit(2)
end
entry[2](arguments)
calogExit(0)