Document the PHP api layer and fix what documenting it exposed

Writing down what each endpoint actually guarantees made the places it
didn't obvious — shell arguments reaching a crontab or a bash -c
unescaped, master.conf written without tmp+rename, and conf edits that
could be saved without ever being parsed.
This commit is contained in:
Gmer4Lfe
2026-08-02 10:11:39 -04:00
parent 6a959fb5e4
commit 987313e7dc
55 changed files with 3972 additions and 95 deletions
+87
View File
@@ -1,4 +1,91 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Structured conf editing. GET returns the configuration fields relevant to one script,
// grouped and typed for rendering as a form; POST writes a set of field changes back to
// whichever conf files they belong to.
//
// OPERATIONAL MODEL
// The counterpart to rawconf.php. That one hands over a text file; this one presents the
// subset of keys a given script actually reads, with their types and current values, so a
// threshold can be changed without opening master.conf and finding it.
//
// Changes are keyed by file, not by form. Each change carries its own target file, because
// one script's settings routinely span master.conf and a host conf — a threshold is shared,
// the credential it applies to is not. A single save therefore writes to several files, and
// reports per-file results.
//
// A master.conf write is followed by a push to every partner, matching rawconf.php. The
// two endpoints edit the same file and must distribute it the same way.
//
// DESIGN PRINCIPLES
// Which fields belong to a script is derived, not configured.
// vv_conf_fields_for_script() resolves them from the script itself, so a new conf
// variable appears in the form as soon as the script reads it — there is no second list
// to keep in step.
//
// Validation is total before any write begins.
// Every change in the set is checked first, and the endpoint exits on the first bad
// one. A partially applied save across multiple conf files is far harder to reason
// about than a rejected one.
//
// ok reflects the whole set.
// ok is false if any file failed, while files carries the per-file detail. A caller
// that checks only ok is correct but coarse; one that wants to know which file failed
// can see it.
//
// OPERATIONAL SAFEGUARDS
// Every change names its own file, and every one is checked against the allowlist.
// in_array(..., true) against vv_get_conf_files() per change — not once for the batch.
// The allowlist is what enforces sparse checkout: HOST2 cannot be handed a change
// targeting host1.conf, because host1.conf is not in its list.
//
// Keys must look like shell variables.
// ^[A-Z_][A-Z0-9_]*$ — no lowercase, no punctuation, no leading digit. The key is used
// to locate and rewrite an assignment in a bash file, so anything that could not be a
// variable name has no legitimate target.
//
// Malformed payloads are refused, not coerced.
// is_array() on the decoded changes, and an explicit missing-id check. A truncated body
// becomes a rejection rather than an empty change set that would report success while
// writing nothing.
//
// The GET path blocks traversal on the id.
// An explicit '..' check before the id reaches the field resolver.
//
// Writes are atomic per file — vv_conf_write_changes() goes through the same tmp + rename
// path as every other conf write, so a script sourcing a conf mid-save sees the old file or
// the new one.
//
// The push only happens after master.conf is confirmed written.
// Guarded on the per-file result being exactly true, so a failed edit cannot distribute
// a stale or partly-written master.conf to partners.
//
// Unknown methods are refused explicitly, so a PUT or DELETE cannot fall through the two
// handled blocks into an empty 200.
//
// Narrower than rawconf.php, but not narrow enough to skip the syntax gate.
// Every write rewrites the value of an existing, named key — no key can be added,
// deleted, or moved. But only the scalar path escapes its value: the array,
// array_single and assoc_array paths splice the caller's text into the file verbatim,
// and the type is chosen by the request. vv_conf_write_changes() therefore runs the
// same `bash -n` check config.php and rawconf.php apply, and a file that does not parse
// is reported as a failed write with the original left intact.
//
// REQUEST
// GET ?id=<Category/name.sh>
// POST id=<Category/name.sh> changes=<JSON array of {file, key, value}>
//
// RESPONSE
// GET {"ok":true,"groups":[…]}
// POST {"ok":bool,"files":{"<conf>":bool, …},"push":[{"host","ok","ready","error"}, …]}
// {"ok":false,"error":"Invalid id"|"Missing id"|"Invalid changes"
// |"Unauthorized file: …"|"Invalid key: …"|"Method not allowed"}
//
// DEPENDS ON
// include/confform.php vv_conf_fields_for_script(), vv_conf_write_changes()
// include/config.php vv_get_conf_files(), vv_push_master_conf(), vv_push_setup_state()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php';
require_once dirname(__DIR__) . '/include/confform.php';