# 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:437–439` 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** | | **Explorer (human, Viktor)** | both shares open from the Network view; interactive Explorer **save** landed owned `1000:1000`; write into the read-only share **refused**, folder still empty | #### 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~~ — **PASSED 2026-07-18 (Viktor).** Network → FELHOM → both shares open. He went further than the scripted leg: a real Explorer **save** into `dokumentumok` succeeded and landed on the box owned **1000:1000** (so `force user` holds for an interactive Explorer write, not just a scripted one), and a write into the read-only `filmek` was refused by Windows ("Destination Folder Access Denied") with the folder left **empty** on disk. Slice 1 is fully PROVEN-LIVE; the capability-map row is flipped. - **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).