v0.321.0: FileBrowser's stale password and its own banners; one backup section on the Data tab; install address of an installed app
gates / gates (push) Successful in 43s
gates / gates (push) Successful in 43s
R-1101: the change-it line is gone for FileBrowser; in the generated state the page asks FileBrowser whether
the stored password still logs in (only its own 401/403 counts) and, refused, says so and offers the
new-password press. The rendered FileBrowser config switches off its update notice and its Install-app
prompt. The Data tab's backup part is one section in plain words (no off-drive, no PBS); the export
password eye is a text button. R-1095: /apps/{slug}/install of an installed app 302s to its settings tab.
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -186,7 +186,8 @@ func fbSelfID(do fbHTTPDo, base, token string) (int, error) {
|
||||
// The password is 16 letters and digits (generateValue "password:16") — never a comma, which the CLI's `user,pw`
|
||||
// form would split on. It is on the one-shot container's command line for about a second, inside the box.
|
||||
|
||||
// ErrFileBrowserNotOperator: the reset is offered only while the box does not know the password.
|
||||
// ErrFileBrowserNotOperator: the reset is offered only while the box does not know the password (`operator`, or
|
||||
// `generated` with the stored value refused — R-1101).
|
||||
var ErrFileBrowserNotOperator = fmt.Errorf("the file manager's password is not in the operator state")
|
||||
|
||||
// fileBrowserContainer is the infra container's fixed name (infra.RenderFileBrowserCompose).
|
||||
@@ -203,7 +204,9 @@ func (m *Manager) ResetFileBrowserAdminPassword(ctx context.Context) error {
|
||||
// The base-stack tick must not start the file browser while it is stopped for the CLI.
|
||||
m.infraMu.Lock()
|
||||
defer m.infraMu.Unlock()
|
||||
if state, _, _ := m.settings.GetFileBrowserAdmin(); state != settings.FileBrowserAdminOperator {
|
||||
// v0.321.0 (R-1101): also when the stored password is refused — the household changed it in FileBrowser.
|
||||
if state, _, _ := m.settings.GetFileBrowserAdmin(); state != settings.FileBrowserAdminOperator &&
|
||||
!(state == settings.FileBrowserAdminGenerated && m.FileBrowserStoredPasswordRefused()) {
|
||||
return ErrFileBrowserNotOperator
|
||||
}
|
||||
if len(m.encKey) == 0 {
|
||||
@@ -303,3 +306,44 @@ func fileBrowserCLISetPassword(ctx context.Context, pw string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── v0.321.0 (R-1101): the stored password, checked against FileBrowser ─────────────────────────────────────────────
|
||||
//
|
||||
// In the `generated` state the box shows the password it set. If the household changed it in FileBrowser, that value
|
||||
// is refused, and the page used to keep offering it. The page now asks FileBrowser first (once per page load, the same
|
||||
// login R-513 uses, the value never logged) and, on a refusal, says so and offers the new-password press.
|
||||
|
||||
// fbCheckTimeout bounds the page's check, so a FileBrowser that does not answer never holds the page.
|
||||
const fbCheckTimeout = 3 * time.Second
|
||||
|
||||
// FileBrowserStoredPasswordRefused reports true ONLY when FileBrowser positively refuses the stored password (401/403)
|
||||
// in the `generated` state. Anything else — another state, no stored value, a decrypt error, FileBrowser not
|
||||
// answering — is false: an unproven refusal must not tell the household its password changed. Pinned by
|
||||
// TestFileBrowserStoredPassword_RefusedOnlyOnA401.
|
||||
func (m *Manager) FileBrowserStoredPasswordRefused() bool {
|
||||
if m.settings == nil || len(m.encKey) == 0 {
|
||||
return false
|
||||
}
|
||||
state, enc, _ := m.settings.GetFileBrowserAdmin()
|
||||
if state != settings.FileBrowserAdminGenerated || enc == "" {
|
||||
return false
|
||||
}
|
||||
pw, err := crypto.Decrypt(m.encKey, enc)
|
||||
if err != nil || strings.TrimSpace(pw) == "" {
|
||||
return false
|
||||
}
|
||||
do := m.fbHTTP
|
||||
if do == nil {
|
||||
do = (&http.Client{Timeout: fbCheckTimeout}).Do
|
||||
}
|
||||
base := fileBrowserBaseURL
|
||||
if m.fbBaseURL != "" {
|
||||
base = m.fbBaseURL
|
||||
}
|
||||
_, code, err := fbLogin(do, base, pw)
|
||||
refused := err == nil && (code == http.StatusUnauthorized || code == http.StatusForbidden)
|
||||
if refused {
|
||||
m.logger.Printf("[WARN] [infra] filebrowser: the stored admin password is refused (HTTP %d) — changed in FileBrowser; the page offers a new one (value never logged)", code)
|
||||
}
|
||||
return refused
|
||||
}
|
||||
|
||||
@@ -141,3 +141,44 @@ func TestEffectiveLoginKind_DefaultAndUnknown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// R-1101 (v0.321.0): the stored password is called refused ONLY on FileBrowser's own 401.
|
||||
// RED-PROOF (2026-10-16, REPORT): with `refused := err == nil && (...)` replaced by `refused := code != http.StatusOK`,
|
||||
// this failed at „FileBrowser down reads as refused".
|
||||
func TestFileBrowserStoredPassword_RefusedOnlyOnA401(t *testing.T) {
|
||||
fake := &fakeFB{password: "TESTONLY-stored-1"}
|
||||
srv := httptest.NewServer(fake.handler())
|
||||
m, st, key := fbManager(t, srv.URL)
|
||||
enc, err := crypto.Encrypt(key, "TESTONLY-stored-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.SetFileBrowserAdmin(settings.FileBrowserAdminGenerated, enc, "2026-10-16T00:00:00Z"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.FileBrowserStoredPasswordRefused() {
|
||||
t.Fatal("the stored password still logs in, and it reads as refused")
|
||||
}
|
||||
fake.mu.Lock()
|
||||
fake.password = "TESTONLY-changed-in-filebrowser"
|
||||
fake.mu.Unlock()
|
||||
if !m.FileBrowserStoredPasswordRefused() {
|
||||
t.Fatal("FileBrowser refuses the stored password (401), and it does not read as refused")
|
||||
}
|
||||
// The reset is then offered in the generated state, and replaces it.
|
||||
calls := 0
|
||||
m.fbSetPassword = func(_ context.Context, pw string) error {
|
||||
calls++
|
||||
fake.mu.Lock()
|
||||
fake.password = pw
|
||||
fake.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if err := m.ResetFileBrowserAdminPassword(context.Background()); err != nil || calls != 1 {
|
||||
t.Fatalf("reset with a refused stored password: err %v, apply ran %d times", err, calls)
|
||||
}
|
||||
srv.Close()
|
||||
if m.FileBrowserStoredPasswordRefused() {
|
||||
t.Fatal("FileBrowser down reads as refused — an unproven refusal must not tell the household its password changed")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user