Files
felhom-controller/REPORT.md
T
admin a4c82a2651 docs(samba): CHANGELOG v0.144.0 + REPORT + CONTEXT + README §16 + REUSE
Records the R-7 slice 1 ship, the four red-proof outcomes, live-validation
evidence, the two bugs live validation caught, and the Part-4 Step-1 enumeration
finding + the reported design fork (share data classified but not yet in a live
backup run — needs a Viktor ruling, suggested R-7b).
2026-07-18 12:03:24 +02:00

179 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — most recent implementation
## felhom-controller v0.144.0 — „Megosztás": LAN SMB file sharing (R-7 slice 1) — 2026-07-18
**Class:** Implementation. **Shipped:** controller **v0.144.0** + new infra image
**`gitea.dooplex.hu/admin/felhom-samba:1.0.0`**. Deployed + live-validated on demo guest 9201.
### 1. Baselines
| Repo | start `main` @ | Version |
|---|---|---|
| felhom-controller | `ac13966` (v0.143.0) | → **v0.144.0** |
| felhom.eu | `7e0370f` | docs only (hub v0.66.0 / scripts v1.20.0 untouched) |
| felhom-agent | `f222a7b` | NOT touched (v0.90.0) |
| app-catalog-felhom.eu | — | NOT touched (deliberately not a catalog app) |
*(The brief listed `a4a7de3d` for the controller; the live baseline was `ac13966` — the delta is the
2026-07-18 build-root relocation, paths/docs only.)*
### 2. Files created / modified
**Created:** `controller/infra-images/samba/{Dockerfile,entrypoint.sh}`,
`controller/scripts/build-samba-image.sh`, `internal/settings/smb.go` (+test),
`internal/infra/samba.go` (+test), `internal/stacks/samba.go` (+test),
`internal/stacks/samba_classify.go` (+test), `internal/web/sharing_handlers.go` (+test),
`internal/web/templates/sharing.html`.
**Modified:** `settings/settings.go` (2 fields), `infra/infra.go` (`SambaImage` pin),
`stacks/infra.go`, `stacks/manager.go` (3 test seams), `stacks/metadata.go` (samba branch),
`config/config.go` (`alwaysProtectedStacks`), `web/server.go`, `web/inframeta.go`,
`cmd/controller/main.go` (`/api/sharing/` mux), `templates/layout.html`, `templates/icons.html`,
`CHANGELOG.md`.
### 3. Commits
| Hash | Phase |
|---|---|
| `f42f3e0` | Part 0 — felhom-samba image + build helper |
| `b0c5ef4` | Part 1 — settings registry + renderers |
| `0dcbea9` | Part 2 — lifecycle (ensure/reconcile/password/disable) + protection |
| `1d26a69` | Part 4 — backup classification from the shares registry |
| `4f08e5e` | Part 3 — „Megosztás" page + guarded picker |
| `2eef9b2` | fix: `/api/sharing/` mux registration (found by live validation) |
| `b409f5e` | fix: storage-ROOT vs share-TARGET validation split (found by live validation) |
### 4. Part-4 Step-1 enumeration finding (verbatim, as required)
`GetStackClassifiedBinds` is **NOT inert** — it is consumed by `backup/offbox_capture.go:36`
(offsite), `backup/tier2_capture.go:45` (tier-2), `appexport/fabplan.go:33,115` (.fab). The
enumeration nevertheless never reaches a share-only stack:
- **Tier-2** — `RunAllTier2` (`tier2.go:399`) iterates `ListDeployedStacks()` and does **not** skip
protected stacks. But `RunTier2` (`tier2.go:269`) resolves `GetAppDrivePath(stack)` then
`os.Stat(RecoveryUnitPath(...))` and **returns nil before `tier2CaptureSet` is ever called** when no
recovery unit exists. samba has neither an app drive path nor a recovery unit.
- **Offsite** — `offbox.go:600` enumerates `settings.GetOffboxApps()` (apps with
`AppBackup[name].Offbox`); samba is not an app-backup app.
- **Volume dumps** — `backup.go:437439` iterates `ListDeployedStacks()` and `continue`s on
`cfg.IsProtectedStack(name)`; with samba protected this correctly skips it.
**Verdict — design fork STOPPED and reported, not improvised** (per the task's Part-4 clause): putting
share data into a live tier-2/offsite run requires teaching both engines about a stack with **no
recovery unit and no single app drive path** — a structural change inside the engines, far beyond an
enumeration tweak. Slice 1 therefore delivers the **correct classification seam** only.
**Consequence, stated plainly:** [R4]'s intent — a customer dropping family files onto `\\FELHOM` gets
the product backup promise — is **classified but not executed**. Share data is in no live backup run
today. Needs a Viktor ruling (suggested new item **R-7b**).
### 5. Tests
Test functions **651 → 669** (+18). Green gate (`go build ./... && go vet ./... && go test ./...`) run
after every phase: **23/23 packages ok**, zero failures. Template gates green
(`template_id_gate.py`, `emoji_gate.py`).
| Scenario | Tests | Result |
|---|---|---|
| A — enable + first share | `TestSambaReconcile_HappyPath`, `TestRenderSambaConfig_Golden`, settings CRUD | PASS |
| B — read-only share | `TestRenderSambaCompose` (`read only = yes` + `:ro` bind) | PASS |
| C — security gates | `TestSharingResolvePath_{GuardMatrix,SymlinkEscapeRefused,DecommissionedRootRefused,UniformRefusal}`, `TestSharingResolveStorageRoot`, `TestPathWithin_*`, name validation | PASS (symlink case SKIPs on Windows — no privilege) |
| D — classification | `TestSambaClassifiedBinds_{TierMembership,ExcludesConfigMounts,NoShares}` | PASS |
| E — delete/disable keep data | `TestSambaShareDeleteAndDisableKeepData` (byte-identical tree) | PASS |
| Idempotency | `TestSambaReconcile_IdempotentNoComposeCall` (seam asserts 0 compose calls) | PASS |
| Secrets | `TestSambaPasswordNeverPersisted`, `TestSambaPasswordRefusedWhenDisabled` | PASS |
#### Red-proofs — all four run, confirmed, restored (`git diff` clean each time)
1. **Classification** — inverted Offsite→mandatory → `TestSambaClassifiedBinds_TierMembership` FAILED:
`Felhőmentés ON must be mandatory, got "optional"` + `mandatory share must be in the OFFSITE set: []`.
2. **`:ro` bind** — dropped the read-only bind mapping → `TestRenderSambaCompose` FAILED:
`read-only share must get a :ro bind, missing "- …/filmek:…/filmek:ro"`.
3. **Picker guard** — removed the containment assert in `sharingOwningRoot`
`TestSharingResolvePath_GuardMatrix` FAILED:
`outside every registered root: must be refused, got "…\Temp\…\002"`.
4. **Idempotency** — removed config change-detection → `TestSambaReconcile_IdempotentNoComposeCall`
FAILED: `unchanged registry must perform NO compose call: calls went 1 → 2`.
#### Password-leak check (rule 4)
`TestSambaPasswordNeverPersisted` asserts the secret reaches the smbpasswd seam and appears in **none**
of `settings.json`, `smb.conf`, `docker-compose.yml`. Live: the demo `settings.json` carries only
`"user_set": true`. The password is STDIN-only (never argv) and never logged.
### 6. Deployment (live)
```
felhom-controller: gitea.dooplex.hu/admin/felhom-controller:0.144.0 Up (healthy)
felhom-samba: gitea.dooplex.hu/admin/felhom-samba:1.0.0 Up
```
Anonymous pull of the samba image verified from guest 9201 without registry creds (Part-0 gate).
Binds (`docker inspect felhom-samba`) — Scenario B proof:
```
/opt/docker/stacks/samba/smb.conf -> /etc/samba/smb.conf rw=false
/mnt/felhom-drives/hdd_1/media/filmek -> …/media/filmek rw=false (read-only share)
/mnt/felhom-drives/hdd_1/shares/dokumentumok -> …/shares/dokumentumok rw=true
samba-passdb volume -> /var/lib/samba rw=true
```
Listeners LAN-only (docker 172.x bridges absent — `bind interfaces only` working):
`445` on `127.0.0.1` + `192.168.0.127`, `137/138` NetBIOS, `3702` WSD, `5357`.
### 7. Live validation
**Method:** the UI is password-gated and no browser leg was used, so validation drove the **exact
endpoints the UI posts to** (curl with a real session cookie + `_csrf` token scraped from the rendered
page) from inside guest 9201. No server logic skipped — only client rendering.
| Leg | Evidence |
|---|---|
| login + `GET /sharing` | 302 login; page renders (title + nav present) |
| `POST /sharing/enable` | 303; `smb.enabled=true, server_name=FELHOM` |
| `POST /sharing/password` | 303; `user_set=true`; log `household SMB password applied for user=felhom` |
| `POST /sharing/shares` (new folder) | 303; `…/hdd_1/shares/dokumentumok` created **owned 1000:1000** |
| `POST /sharing/shares` (existing, read-only) | 303; stored `read_only: true` |
| Guard refusals (`/api/sharing/browse`) | `appdata`, `backups`, `/etc`, drive-root → **all 400, identical** `{"error":"Ez a mappa nem osztható meg."}` |
| Guard accept | `…/hdd_1/media` → 200 with entries |
| Generated `smb.conf` | hardened global verbatim; `[filmek] read only = yes`, `[dokumentumok] read only = no`, force-user block on both |
| Windows 11 (192.168.0.110) | `Test-NetConnection 445`**True** |
| NetBIOS flat name (nmbd) | `nbtstat`: `FELHOM <00>/<03>/<20> Registered`; `ping FELHOM``Reply from 192.168.0.127` |
| SMB round-trip (writable) | write → readback **BYTE-COMPARE: PASS** |
| **Write-refused (read-only)** | write to `\\192.168.0.127\filmek` **rejected**; folder still empty on the box (non-effect proven) |
| `force user` promise | SMB-written `r7.txt` owned **1000:1000** on the box |
| WS-Discovery | ProbeMatch from `192.168.0.127` **PASS** |
#### Two real bugs caught by live validation (fixed + regression-tested)
1. **`/api/sharing/browse` unreachable** — the `/api/` subtree is routed on the main mux in `main.go`,
so the case added to the web `ServeHTTP` switch was shadowed by the apiRouter catch-all and
returned `401 authentication required`. Fixed by registering
`mux.Handle("/api/sharing/", RequireAuth(CsrfProtect(ServeSharingAPI)))`, matching `/api/storage/`.
2. **Share creation silently failed** — the "new folder" flow passed the storage ROOT through
`sharingResolvePath`, which correctly refuses the drive root as a share TARGET. Split out
`sharingResolveStorageRoot` (accepts exactly a registered live root — strictly tighter);
`TestSharingResolveStorageRoot` asserts both halves.
### 8. NOT yet live-validated
- **Explorer render — Viktor's human leg.** Network view shows `FELHOM`, double-click opens both
shares. Everything objective around it passed (ProbeMatch, 445, NetBIOS name resolution, by-name
mount), so this is UX confirmation, not a mechanism gate. Stack + both shares are live on the demo box.
- **Share data in a real backup run** — blocked by the §4 design fork (classified, not executed).
- **Symlink-escape guard on Linux** — unit test SKIPs on Windows (no privilege).
- Drive-disconnect behaviour live (unit-tested only); multi-root share sets; SMB throughput/signing.
### 9. Observations (not acted on)
- `monitor.EffectiveProtected` derives only from `cfg.Stacks.Protected` (golden-generated), so samba is
**not** liveness-monitored — a dead samba container raises no "protected container missing" issue.
Fixing it needs the golden controller.yaml to list samba, or the SMB-enabled flag threaded through 4
`RunHealthCheck` call sites. Deferred deliberately; the shipped direction is the safe one (no false
alarms while the feature is off).
- A classified bind's `RelPath` is relative to its owning storage root; a share set spanning
**multiple** roots has no single resolution root in the current `(Root, RelPath) + hddPath`
vocabulary — part of the §4 fork.
- `sambaWriteAtomic` is a fourth atomic-write helper (with `backup.atomicWrite`,
`bootstrap.writeFileAtomic`, `setup.atomicWriteFile`) — a shared util is worth a cleanup pass.
- A throwaway `r7.txt` from the round-trip test remains in the demo `dokumentumok` share (harmless;
left so the Explorer leg has visible content).