196 lines
7.5 KiB
Lua
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)
|