diff --git a/pkg/inputunifi/input.go b/pkg/inputunifi/input.go index a1e3dbe5..c18d1aa6 100644 --- a/pkg/inputunifi/input.go +++ b/pkg/inputunifi/input.go @@ -186,6 +186,16 @@ func (u *InputUnifi) getUnifi(c *Controller) error { DebugLog: u.LogDebugf, } + // Nothing on a Protect-only console uses a session unless Protect logs are wanted: the + // Integration API authenticates with the key alone. Withholding the credentials here + // keeps NewProtectClient from attempting -- and logging -- a login on every re-auth, + // which matters because an unset user is filled in above with a placeholder that no + // console will ever accept. + if *c.DisableNetwork && !*c.SaveProtectLogs { + cfg.User = "" + cfg.Pass = "" + } + var lastErr error backoff := 30 * time.Second diff --git a/pkg/inputunifi/input_test.go b/pkg/inputunifi/input_test.go index 116ba4be..3272c30b 100644 --- a/pkg/inputunifi/input_test.go +++ b/pkg/inputunifi/input_test.go @@ -280,6 +280,44 @@ func TestProtectOnlyWarnsWithoutAPIKey(t *testing.T) { assert.Contains(t, logger.errors(), "no protect_api_key") } +// A key-only config is what an operator actually writes for a UNVR, and setDefaults fills the +// unset user with "unifipoller". Sending that placeholder made the console answer 403 and +// killed the controller outright, so the credentials must not be sent when no session can be +// used -- and the startup summary must not claim an authentication that never happens. +func TestProtectOnlyDoesNotSendUnusedCredentials(t *testing.T) { + t.Parallel() + + fake := &unvr{} + srv := fake.start(t) + + logger := &captureLogger{} + u := newProtectOnlyInput(srv.URL, func(c *inputunifi.Controller) { + c.User = "" + c.Pass = "" + }) + require.NoError(t, u.Initialize(logger)) + + a := assert.New(t) + require.NotNil(t, u.Controllers[0].Unifi) + a.NotContains(fake.paths(), unifi.APILoginPathNew, "no session is usable, so none should be attempted") + a.Contains(logger.infoLines(), "Protect API key only (no session needed)") + a.NotContains(logger.infoLines(), "unifipoller") +} + +// With Protect logs enabled the session is genuinely needed, so the credentials are sent. +func TestProtectOnlyLogsInForProtectLogs(t *testing.T) { + t.Parallel() + + enabled := true + fake := &unvr{} + srv := fake.start(t) + + u := newProtectOnlyInput(srv.URL, func(c *inputunifi.Controller) { c.SaveProtectLogs = &enabled }) + require.NoError(t, u.Initialize(nil)) + + assert.Contains(t, fake.paths(), unifi.APILoginPathNew) +} + // disable_network has to survive four independent binding paths -- toml, json, yaml and the // UP_ environment -- each driven by its own struct tag. A typo in any one tag leaves the // option silently inert for users of that format. Note the env name derives from the xml tag. @@ -355,13 +393,20 @@ func TestShippedExamplesDoNotDisableNetwork(t *testing.T) { // captureLogger records what Initialize logs, so the startup warnings can be asserted on. type captureLogger struct { - mu sync.Mutex - errs []string + mu sync.Mutex + errs []string + infos []string } -func (l *captureLogger) Logf(string, ...any) {} func (l *captureLogger) LogDebugf(string, ...any) {} +func (l *captureLogger) Logf(msg string, v ...any) { + l.mu.Lock() + defer l.mu.Unlock() + + l.infos = append(l.infos, fmt.Sprintf(msg, v...)) +} + func (l *captureLogger) LogErrorf(msg string, v ...any) { l.mu.Lock() defer l.mu.Unlock() @@ -375,3 +420,10 @@ func (l *captureLogger) errors() string { return strings.Join(l.errs, "\n") } + +func (l *captureLogger) infoLines() string { + l.mu.Lock() + defer l.mu.Unlock() + + return strings.Join(l.infos, "\n") +} diff --git a/pkg/inputunifi/interface.go b/pkg/inputunifi/interface.go index fb1171be..b399ccba 100644 --- a/pkg/inputunifi/interface.go +++ b/pkg/inputunifi/interface.go @@ -191,9 +191,16 @@ func (u *InputUnifi) logController(c *Controller) { u.Logf(" => Version: %s (%s)", c.Unifi.ServerVersion, c.Unifi.UUID) } - if c.Remote { + switch { + case c.Remote: u.Logf(" => API Key: %v", c.APIKey != "") - } else { + case *c.DisableNetwork && !*c.SaveProtectLogs: + // getUnifi withholds the credentials entirely in this case, so naming a user here + // would describe an authentication that never happens. + u.Logf(" => Auth: Protect API key only (no session needed)") + case *c.DisableNetwork: + u.Logf(" => Username: %s (has password: %v) — used only for Protect logs", c.User, c.Pass != "") + default: u.Logf(" => Username: %s (has password: %v) (has api-key: %v)", c.User, c.Pass != "", c.APIKey != "") }