mirror of
https://github.com/unpoller/unpoller.git
synced 2026-09-30 03:21:28 +02:00
fix: don't send unused credentials to a Protect-only console
Found by running this against a real UNVR4 rather than only the fake one. setDefaults fills an unset user with the placeholder "unifipoller". For a Protect-only console that placeholder was reaching Login(), the console answered 403, and the controller died during initialisation -- the same symptom #1066 set out to fix, one layer further in. A Protect API key and no local account is the config an operator actually writes for a UNVR, so this was the common case, not an edge one. Nothing on such a console uses a session unless Protect logs are wanted: the Integration API authenticates with the key alone. So withhold the credentials entirely in that case, and say so in the startup summary rather than naming a username that is never sent. Also pins the go.mod bump to unifi v6.0.1, which is the release that carries NewProtectClient (unpoller/unifi#240). Verified end to end against a UNVR4 (UniFi OS 5.1.31, Protect 7.2.105): the controller comes up, logs "Auth: Protect API key only (no session needed)", makes no login request at all, and exports 37 unpoller_protect_* series across 9 cameras and 2 bridges with unpoller_controller_up = 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVreutpEATmBjm6PBw9RjQ
This commit is contained in:
co-authored by
Claude Opus 5
parent
31cc5a0f31
commit
a73a2e0ab2
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 != "")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user