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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user