R-1281: FileBrowser is served on its own felhom-files network (traefik, and OnlyOffice while linked) and leaves traefik-public, so no app container reaches it. R-1126's check-then-publish gate unchanged. Red-proved. Commit only.
gates / gates (push) Successful in 1m29s
gates / gates (push) Successful in 1m29s
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/dockerexec"
|
||||
@@ -68,13 +70,16 @@ func (m *Manager) joinFileBrowserNetwork() {
|
||||
m.logger.Printf("[INFO] [infra] connected %s to %s (the FileBrowser default-login check, R-1126)", controllerContainer, infra.FileBrowserNetwork)
|
||||
}
|
||||
|
||||
// ensureFileBrowserPublic joins FileBrowser to traefik-public only when THIS run was checked and its default login is
|
||||
// refused. Anything less — the run unknown, not checked yet, the check failed — leaves it unreachable from outside.
|
||||
// ensureFileBrowserPublic serves files.<domain> only when THIS run was checked and its default login is refused:
|
||||
// traefik joins infra.FilesNetwork, then the checked FileBrowser container does — never traefik-public, where every app
|
||||
// container is (R-1281). A FileBrowser an earlier controller joined to traefik-public leaves it once it is on the files
|
||||
// network. OnlyOffice joins the files network only while its FileBrowser link is on. Anything less than a checked run
|
||||
// leaves FileBrowser unreachable from outside, as R-1126 requires.
|
||||
//
|
||||
// TOCTOU (found by the commit's security review, 2026-10-17): the connect targets the CONTAINER ID read with the run,
|
||||
// never the name — web.SyncFileBrowserMounts can re-create FileBrowser between the check and the connect, and a
|
||||
// connect by name would publish the new, unchecked container. By id, a replaced container's connect simply fails.
|
||||
// Pinned by TestFileBrowserGate_ConnectsTheCheckedContainerById.
|
||||
// Pinned by TestFileBrowserGate_ConnectsTheCheckedContainerById and TestR1281_*.
|
||||
func (m *Manager) ensureFileBrowserPublic() {
|
||||
run := m.fbStartedAt()
|
||||
if run == "" || run != m.fbProbedRun {
|
||||
@@ -84,14 +89,107 @@ func (m *Manager) ensureFileBrowserPublic() {
|
||||
if f := strings.Fields(run); len(f) == 2 {
|
||||
target = f[0]
|
||||
}
|
||||
if err := m.fbNetEnsure(infra.FilesNetwork); err != nil {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: the %s network is not there (files.<domain> stays as it is): %v", infra.FilesNetwork, err)
|
||||
return
|
||||
}
|
||||
if !m.fbNetOn(traefikContainer, infra.FilesNetwork) {
|
||||
if err := m.fbNetConnect(traefikContainer, infra.FilesNetwork); err != nil {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: traefik could not join %s (files.<domain> stays as it is): %v", infra.FilesNetwork, err)
|
||||
return
|
||||
}
|
||||
m.logger.Printf("[INFO] [infra] filebrowser: traefik joined %s — the only route to FileBrowser (R-1281)", infra.FilesNetwork)
|
||||
}
|
||||
if !m.fbNetOn(target, infra.FilesNetwork) {
|
||||
if err := m.fbNetConnect(target, infra.FilesNetwork); err != nil {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: could not join %s (files.<domain> stays unreachable): %v", infra.FilesNetwork, err)
|
||||
return
|
||||
}
|
||||
m.logger.Printf("[INFO] [infra] filebrowser: default login refused for this run — joined %s, files.<domain> served (R-1126, R-1281)", infra.FilesNetwork)
|
||||
}
|
||||
if m.fbNetOn(target, traefikNetwork) {
|
||||
return
|
||||
if err := m.fbNetDisconnect(target, traefikNetwork); err != nil {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: could not leave %s — every app can still reach it (R-1281): %v", traefikNetwork, err)
|
||||
} else {
|
||||
m.logger.Printf("[INFO] [infra] filebrowser: left %s — no app container reaches it any more (R-1281)", traefikNetwork)
|
||||
}
|
||||
}
|
||||
if err := m.fbNetConnect(target, traefikNetwork); err != nil {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: could not join %s (files.<domain> stays unreachable): %v", traefikNetwork, err)
|
||||
return
|
||||
m.ensureOfficeOnFilesNetwork()
|
||||
}
|
||||
|
||||
// traefikContainer is traefik's container (infra/templates/traefik-compose.yml.tmpl container_name).
|
||||
const traefikContainer = "traefik"
|
||||
|
||||
// officeContainer is OnlyOffice's container (the catalog's container_name).
|
||||
const officeContainer = "onlyoffice"
|
||||
|
||||
// ensureOfficeOnFilesNetwork (R-1281): OnlyOffice shares the files network while — and only while — its FileBrowser
|
||||
// link is on (FileBrowser's config holds the office block). Called after FileBrowser was published, so it never reaches
|
||||
// an unchecked FileBrowser.
|
||||
func (m *Manager) ensureOfficeOnFilesNetwork() {
|
||||
linked, running := m.fbOfficeLinked()
|
||||
on := m.fbNetOn(officeContainer, infra.FilesNetwork)
|
||||
switch {
|
||||
case linked && running && !on:
|
||||
if err := m.fbNetConnect(officeContainer, infra.FilesNetwork); err != nil {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: OnlyOffice could not join %s (documents do not open in the file manager): %v", infra.FilesNetwork, err)
|
||||
return
|
||||
}
|
||||
m.logger.Printf("[INFO] [infra] filebrowser: OnlyOffice joined %s — its FileBrowser link is on (R-1281)", infra.FilesNetwork)
|
||||
case !linked && on:
|
||||
if err := m.fbNetDisconnect(officeContainer, infra.FilesNetwork); err != nil {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: OnlyOffice could not leave %s: %v", infra.FilesNetwork, err)
|
||||
return
|
||||
}
|
||||
m.logger.Printf("[INFO] [infra] filebrowser: OnlyOffice left %s — its FileBrowser link is off (R-1281)", infra.FilesNetwork)
|
||||
}
|
||||
m.logger.Printf("[INFO] [infra] filebrowser: default login refused for this run — joined %s, files.<domain> served (R-1126)", traefikNetwork)
|
||||
}
|
||||
|
||||
// fbNetDisconnect detaches container from network (test seam: fbNetDisconnectFn; a test FileBrowser and no seam → no-op).
|
||||
func (m *Manager) fbNetDisconnect(container, network string) error {
|
||||
if m.fbNetDisconnectFn != nil {
|
||||
return m.fbNetDisconnectFn(container, network)
|
||||
}
|
||||
if m.fbHTTP != nil || m.fbBaseURL != "" {
|
||||
return nil
|
||||
}
|
||||
out, err := dockerexec.Command("docker", "network", "disconnect", network, container).CombinedOutput()
|
||||
if err != nil && !strings.Contains(string(out), "is not connected") {
|
||||
return &netConnectError{out: strings.TrimSpace(string(out)), err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fbNetEnsure creates network when it does not exist (test seam: fbNetEnsureFn; a test FileBrowser and no seam → no-op).
|
||||
func (m *Manager) fbNetEnsure(network string) error {
|
||||
if m.fbNetEnsureFn != nil {
|
||||
return m.fbNetEnsureFn(network)
|
||||
}
|
||||
if m.fbHTTP != nil || m.fbBaseURL != "" {
|
||||
return nil
|
||||
}
|
||||
if err := dockerexec.Command("docker", "network", "inspect", network).Run(); err == nil {
|
||||
return nil
|
||||
}
|
||||
out, err := dockerexec.Command("docker", "network", "create", network).CombinedOutput()
|
||||
if err != nil && !strings.Contains(string(out), "already exists") {
|
||||
return &netConnectError{out: strings.TrimSpace(string(out)), err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fbOfficeLinked reports whether FileBrowser's config holds the OnlyOffice block and whether OnlyOffice runs (test
|
||||
// seam: fbOfficeLinkedFn; a test FileBrowser and no seam → neither).
|
||||
func (m *Manager) fbOfficeLinked() (bool, bool) {
|
||||
if m.fbOfficeLinkedFn != nil {
|
||||
return m.fbOfficeLinkedFn()
|
||||
}
|
||||
if m.fbHTTP != nil || m.fbBaseURL != "" {
|
||||
return false, false
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(m.cfg.Paths.StacksDir, "filebrowser", "config.yaml"))
|
||||
linked := err == nil && strings.Contains(string(b), "\nintegrations:\n office:")
|
||||
return linked, containerRunning(officeContainer)
|
||||
}
|
||||
|
||||
// EnsureFileBrowserGate is the whole check outside the base-stack round — called after web.SyncFileBrowserMounts
|
||||
|
||||
Reference in New Issue
Block a user