docs+server: document the .zddc bootstrap config + warn at startup
A fresh ZDDC deployment grants no access to anyone until an operator
populates the root .zddc (admins) and per-project .zddc files (role
members). Until now this was only documented in comments inside the
embedded defaults.zddc.yaml, surfaced via `zddc-server show-defaults`
— operators wiring up a fresh master had no obvious doc to follow and
no startup signal when the bootstrap was missing or empty.
- README.md: new "## Deploy: bootstrap config" section between Tools
and File-naming convention. Two canonical examples (root admin-only,
per-project role members), schema essentials (verb bits, principal
forms, admins-only-at-root), and the acl: { allow: [...] } footgun
that silently drops grants.
- AGENTS.md: new "### Bootstrap config (REQUIRED — unlocks the server)"
subsection at the top of ## zddc-server. Same content as README but
with file:line citations into zddc/internal/zddc/file.go for the
schema source of truth.
- zddc-server: new warnIfNoBootstrap fires a slog.Warn at startup when
the root .zddc grants nobody anything (no admins, no acl.permissions,
no role members). Master mode only; skipped under --no-auth.
- config validator's existing no-root-.zddc fail-fast error message now
also points at the new README + AGENTS sections so all three signals
(fail-fast, runtime warning, docs) converge.
Smoke-tested all paths: empty root + default (fail-fast), empty root +
--insecure (file-missing warn), admins-only / perms-only / role-members
-only (silent), title-only and acl.allow footgun (both warn), --no-auth
(suppressed). All existing go tests pass.
Follow-up (manual, separate repo): add an analogous section to
~/src/zddc-website/reference.html.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
69878532b0
commit
cd05cd6366
4 changed files with 129 additions and 1 deletions
40
AGENTS.md
40
AGENTS.md
|
|
@ -482,6 +482,46 @@ This is a guideline, not a rule. Revisit per-feature: when v1+ form-spec adds `$
|
|||
|
||||
Go HTTP server sub-project living at `zddc/`. Replaces `caddy file-server --browse` for ZDDC archives.
|
||||
|
||||
### Bootstrap config (REQUIRED — unlocks the server)
|
||||
|
||||
zddc-server grants no access to anyone until two operator files are populated. The embedded `defaults.zddc.yaml` ships with empty role members and references those roles throughout its cascade, so a fresh deployment refuses every request until the operator opts in. `zddc-server` logs a startup warning (see `warnIfNoBootstrap` in `zddc/cmd/zddc-server/main.go`) when the root `.zddc` grants nobody anything — skipped under `--no-auth`.
|
||||
|
||||
**Root `<ZDDC_ROOT>/.zddc`** — at minimum, declare an admin:
|
||||
|
||||
```yaml
|
||||
admins:
|
||||
- cwitt@burnsmcd.com
|
||||
```
|
||||
|
||||
`admins:` is honored only at the root (subdir admins are read but ignored by `IsAdmin`, see `zddc/internal/zddc/file.go:109-112`). Admins are sudo-style — powers gate on the `zddc-elevate=1` cookie or implicit bearer-token elevation.
|
||||
|
||||
**Per-project `<project>/.zddc`** — populate role members:
|
||||
|
||||
```yaml
|
||||
title: "Project Phoenix"
|
||||
roles:
|
||||
document_controller:
|
||||
members:
|
||||
- dc1@burnsmcd.com
|
||||
project_team:
|
||||
members:
|
||||
- alice@burnsmcd.com
|
||||
- '*@acme.com'
|
||||
```
|
||||
|
||||
The embedded cascade already grants `project_team: r` project-wide and `document_controller: rw` (+ `rwc` on `archive/`, WORM filing on `received/issued`, subtree-admin on `working/`/`staging/`/`reviewing/`). Populating role members lights all of that up.
|
||||
|
||||
**Schema** (source of truth: `zddc/internal/zddc/file.go:43-49`, `:74-77`, `:139-145`):
|
||||
|
||||
- `acl: { permissions: { <principal>: <bits> }, inherit: <bool>? }` — there is no `allow:` key; an `allow:` block parses cleanly but is silently dropped during unmarshal. Real footgun — easy to write `acl: { allow: [...] }` and assume it works.
|
||||
- Bits: any subset of `r w c d a` (read / write / create / delete / admin); empty string is an explicit deny.
|
||||
- Principals: email (must contain `@`), glob (`*@domain.com`), or role name (no `@`).
|
||||
- `roles: { <name>: { members: [...], reset: <bool>? } }` — members union across the cascade unless `reset: true`.
|
||||
- `admins: [<email>, ...]` — root only; sudo-style elevation per request.
|
||||
- `title:` — read only from the per-project `.zddc`; surfaces on the landing-page picker.
|
||||
|
||||
Run `zddc-server show-defaults` to dump the embedded `defaults.zddc.yaml` with annotated comments — that's the full schema with all the cascade keys (`worm:`, `auto_own:`, `drop_target:`, `apps:`, `convert:`, `on_plan_review:`, `records:`, `available_tools:`, `default_tool:`, `dir_tool:`, etc.).
|
||||
|
||||
### Build
|
||||
|
||||
zddc-server ships as a cross-compiled binary, not a container image. There's no Containerfile or compose file in this repo (the chart Dockerfiles compile from source at deploy time at the right tag).
|
||||
|
|
|
|||
41
README.md
41
README.md
|
|
@ -22,6 +22,47 @@ The name "Zero Day Document Control" comes from the convention itself — adopt
|
|||
|
||||
Each tool is published in three channels (stable, beta, alpha) as static files served from <https://zddc.varasys.io/releases/>. **Local use:** download a `.html` file from `releases/` and open it in a browser. **Server use:** run `zddc-server` — the current-stable build of every tool is baked into the binary at compile time, so a fresh deployment Just Works with zero config. Which tool a directory URL serves is driven by the `.zddc` cascade: a baked-in `defaults.zddc.yaml` (dump it with `zddc-server show-defaults`) declares, per folder, `default_tool` (the no-slash form — archive under `archive/`, transmittal under `staging/`, browse under `working/`+`reviewing/` (browse hosts the in-place markdown editor), classifier under `incoming/`, tables at `archive/<party>/mdl`, landing at root) and `dir_tool` (the trailing-slash form; defaults to `browse`); operators override at any level. A `.zip` file is also a navigable directory (`GET …/Foo.zip/`), and `GET /dir/?zip=1` streams an ACL-filtered zip of a subtree. Override the *tool source* per-directory by writing an `apps:` entry in any `.zddc` file (channel/version/URL/path) — fetched once and cached in `<ZDDC_ROOT>/_app/` — or drop a real `.html` file at any path.
|
||||
|
||||
## Deploy: bootstrap config
|
||||
|
||||
> **A fresh `zddc-server` deployment grants no access to anyone until two config files are populated.** Without them, the server runs but every request returns 403. The embedded `defaults.zddc.yaml` ships with empty role members so deployments must opt-in to authorize anyone.
|
||||
|
||||
**Step 1.** At the master root, create `/.zddc` (i.e. `<ZDDC_ROOT>/.zddc`) naming at least one admin:
|
||||
|
||||
```yaml
|
||||
admins:
|
||||
- cwitt@burnsmcd.com
|
||||
```
|
||||
|
||||
`admins:` is honored only at the root file. Admins behave as normal users by default and elevate per-request via the `zddc-elevate=1` cookie (header toggle in every tool) or implicitly when authenticating with a bearer token.
|
||||
|
||||
**Step 2.** In each project, create `<project>/.zddc` to populate the `document_controller` and `project_team` role members:
|
||||
|
||||
```yaml
|
||||
title: "Project Phoenix"
|
||||
roles:
|
||||
document_controller:
|
||||
members:
|
||||
- dc1@burnsmcd.com
|
||||
project_team:
|
||||
members:
|
||||
- alice@burnsmcd.com
|
||||
- '*@acme.com' # external counterparty (glob)
|
||||
```
|
||||
|
||||
That's it. The embedded cascade does the rest — `project_team` gets read across the project; `document_controller` gets write/create authority on the archive subtree, WORM filing rights on `received/issued`, and subtree-admin of `working/`/`staging/`/`reviewing/`.
|
||||
|
||||
**Common footgun.** `acl: { allow: [...] }` is silently ignored (the YAML parses, but `ACLRules` only reads `permissions:`). The correct shape is:
|
||||
|
||||
```yaml
|
||||
acl:
|
||||
permissions:
|
||||
'<principal>': <bits>
|
||||
```
|
||||
|
||||
Bits are any subset of `r w c d a` (read / write / create / delete / admin); empty string is an explicit deny. Principals are emails, globs like `*@domain.com`, or role names (anything without an `@`).
|
||||
|
||||
`zddc-server` prints a startup warning when the root `.zddc` grants nobody anything — watch for it on first boot. For the full schema, run `zddc-server show-defaults` (dumps the embedded `defaults.zddc.yaml` with annotated comments).
|
||||
|
||||
## File-naming convention
|
||||
|
||||
The full specification — filename format, tracking numbers, revision rules, status codes, folder naming, and the transmittal workflow — lives at <https://zddc.varasys.io/reference.html>.
|
||||
|
|
|
|||
|
|
@ -218,6 +218,12 @@ func main() {
|
|||
"cache_ttl", cfg.OPACacheTTL,
|
||||
"no_auth", cfg.NoAuth)
|
||||
|
||||
// Bootstrap sanity: warn loudly (but don't fail) when the root .zddc
|
||||
// grants nobody anything. Embedded defaults.zddc.yaml ships with empty
|
||||
// role members, so a fresh deployment refuses every request until the
|
||||
// operator populates the file.
|
||||
warnIfNoBootstrap(cfg)
|
||||
|
||||
// Token store: bearer-token issuance and validation.
|
||||
// Persists under <ZDDC_ROOT>/.zddc.d/tokens/ — already excluded
|
||||
// from public listings (fs.ListDirectory dot-prefix filter) and
|
||||
|
|
@ -546,6 +552,46 @@ func setupApps(cfg config.Config) (*apps.Server, error) {
|
|||
return apps.NewServer(cfg.Root, cache, fetcher, version), nil
|
||||
}
|
||||
|
||||
// warnIfNoBootstrap fires a startup slog.Warn when the root .zddc grants
|
||||
// nobody anything — the embedded defaults.zddc.yaml ships with empty role
|
||||
// members, so a deployment without operator-populated admins / acl
|
||||
// permissions / role members refuses every request. Skipped under
|
||||
// --no-auth (auth disabled; warning would be redundant). Per-project
|
||||
// .zddc files may legitimately carry all grants, so the warning text
|
||||
// tells the operator they can ignore it in that case.
|
||||
//
|
||||
// Master-mode only — the bootstrap concept doesn't apply in client
|
||||
// (proxy/cache/mirror) mode, where cfg.Root is the cache directory.
|
||||
func warnIfNoBootstrap(cfg config.Config) {
|
||||
if cfg.NoAuth {
|
||||
return
|
||||
}
|
||||
rootPath := filepath.Join(cfg.Root, ".zddc")
|
||||
rootZddc, err := zddc.ParseFile(rootPath)
|
||||
if err != nil {
|
||||
slog.Warn("root .zddc not present or unreadable; ZDDC will refuse every request until you create it. "+
|
||||
"See README.md '## Deploy: bootstrap config' or AGENTS.md '## zddc-server / ### Bootstrap config'.",
|
||||
"path", rootPath, "err", err)
|
||||
return
|
||||
}
|
||||
hasAdmin := len(rootZddc.Admins) > 0
|
||||
hasPerm := len(rootZddc.ACL.Permissions) > 0
|
||||
hasRoleMembers := false
|
||||
for _, role := range rootZddc.Roles {
|
||||
if len(role.Members) > 0 {
|
||||
hasRoleMembers = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAdmin && !hasPerm && !hasRoleMembers {
|
||||
slog.Warn("root .zddc grants nobody anything (no admins, no acl.permissions, no role members). "+
|
||||
"ZDDC will refuse every request until you populate it. "+
|
||||
"If you intentionally grant only at per-project levels, you can ignore this. "+
|
||||
"See README.md '## Deploy: bootstrap config' or AGENTS.md '## zddc-server / ### Bootstrap config'.",
|
||||
"path", rootPath)
|
||||
}
|
||||
}
|
||||
|
||||
// printVersions writes the binary version + the build label of every app
|
||||
// embedded into the binary. Called by --version and reused for the
|
||||
// startup log line.
|
||||
|
|
|
|||
|
|
@ -264,7 +264,8 @@ func Load(args []string) (Config, error) {
|
|||
return Config{}, fmt.Errorf(
|
||||
"no %s/.zddc file found; the served tree would be publicly accessible to anonymous callers. "+
|
||||
"Create a starter .zddc (at minimum: `admins: [you@yourcompany.com]`) "+
|
||||
"or pass --insecure (or ZDDC_INSECURE=1) to acknowledge a deliberately-public deployment",
|
||||
"or pass --insecure (or ZDDC_INSECURE=1) to acknowledge a deliberately-public deployment. "+
|
||||
"See README.md '## Deploy: bootstrap config' or AGENTS.md '## zddc-server / ### Bootstrap config'",
|
||||
cfg.Root)
|
||||
} else if err != nil {
|
||||
return Config{}, fmt.Errorf("could not stat %s/.zddc: %w", cfg.Root, err)
|
||||
|
|
|
|||
Loading…
Reference in a new issue