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

11 KiB
Raw Blame History

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-2RunAllTier2 (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.
  • Offsiteoffbox.go:600 enumerates settings.GetOffboxApps() (apps with AppBackup[name].Offbox); samba is not an app-backup app.
  • Volume dumpsbackup.go:437439 iterates ListDeployedStacks() and continues 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 sharingOwningRootTestSharingResolvePath_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 445True
NetBIOS flat name (nmbd) nbtstat: FELHOM <00>/<03>/<20> Registered; ping FELHOMReply 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).