mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Make agent command-exec token rejection actionable, not a silent 'Invalid token'
An agent enrolled for metrics but whose token the server doesn't recognise (or that lacks the agent:exec scope, or is bound to a different agent) was rejected on the command-exec WebSocket with a bare 'Invalid token' and — for the token-not-found case — no server log at all. The agent then retried forever, logging only 'Invalid token', so the operator had no signal that discovery deep-scan was failing or why. (Confirmed live: delly/minipc agents pointed at a backend that didn't recognise their token retried thousands of times; discovery abstained for every guest as a result.) - agentexec/server.go: the registration-rejection message the agent logs verbatim now says 'agent token not authorized for command execution — re-run the agent installer to enroll an agent:exec-scoped token'. - api/agent_exec_token_binding.go: the previously-silent token-not-recognised branch now logs the specific reason with the agent hostname. Contract-neutral: same rejection behaviour, just legible. Regression test: TestHandleWebSocket_RejectionMessageIsActionable. Verified live end-to-end.
This commit is contained in:
@@ -433,7 +433,12 @@ func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate token
|
||||
if !s.validateToken(reg.Token, reg.AgentID, reg.Hostname) {
|
||||
log.Warn().Str("agent_id", reg.AgentID).Msg("Agent registration rejected: invalid token")
|
||||
rejectedMsg, err := NewMessage(MsgTypeRegistered, "", RegisteredPayload{Success: false, Message: "Invalid token"})
|
||||
// Actionable message instead of a bare "Invalid token": the agent logs
|
||||
// this verbatim, and the dominant causes (token not recognised, or not
|
||||
// bound to this agent) are both fixed by re-enrolling, while a token
|
||||
// that exists but lacks the scope is named explicitly. Avoids the silent
|
||||
// retry loop that previously gave operators nothing to act on.
|
||||
rejectedMsg, err := NewMessage(MsgTypeRegistered, "", RegisteredPayload{Success: false, Message: "agent token not authorized for command execution — re-run the agent installer to enroll an agent:exec-scoped token"})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("agent_id", reg.AgentID).Msg("Failed to encode rejection message")
|
||||
conn.Close()
|
||||
|
||||
@@ -173,6 +173,39 @@ func TestHandleWebSocket_InvalidTokenRejectionSendFailure(t *testing.T) {
|
||||
waitFor(t, 2*time.Second, func() bool { return !s.IsAgentConnected("a1") })
|
||||
}
|
||||
|
||||
func TestHandleWebSocket_RejectionMessageIsActionable(t *testing.T) {
|
||||
// The agent logs the rejection message verbatim, so it must tell the operator
|
||||
// how to fix it (re-enroll / agent:exec scope) rather than the opaque
|
||||
// "Invalid token" that previously left a stale-enrollment agent retrying
|
||||
// forever with no actionable signal.
|
||||
s := NewServer(func(string, string, string) bool { return false })
|
||||
ts := newWSServer(t, s)
|
||||
defer ts.Close()
|
||||
|
||||
conn, _, err := dialAgentExecWebSocket(t, ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
wsWriteMessage(t, conn, mustNewMessage(t, MsgTypeAgentRegister, "", AgentRegisterPayload{
|
||||
AgentID: "a1",
|
||||
Hostname: "host1",
|
||||
Token: "bad",
|
||||
}))
|
||||
|
||||
payload := wsReadRegisteredPayload(t, conn)
|
||||
if payload.Success {
|
||||
t.Fatalf("expected registration to be rejected")
|
||||
}
|
||||
if payload.Message == "Invalid token" {
|
||||
t.Fatalf("rejection message must not be the bare opaque 'Invalid token'")
|
||||
}
|
||||
if !strings.Contains(payload.Message, "re-run the agent installer") {
|
||||
t.Fatalf("rejection message should point the operator at re-enrollment, got: %q", payload.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWebSocket_RegistrationAckSendFailure(t *testing.T) {
|
||||
origWriteTextMessage := writeTextMessage
|
||||
t.Cleanup(func() { writeTextMessage = origWriteTextMessage })
|
||||
|
||||
@@ -25,6 +25,14 @@ func (r *Router) validateAgentExecToken(token string, agentID string, hostname s
|
||||
record, ok := r.config.ValidateAPIToken(token)
|
||||
if !ok {
|
||||
config.Mu.Unlock()
|
||||
// This is the branch a stale-enrollment agent hits: it holds a token
|
||||
// from a prior install that this server no longer recognises. It was
|
||||
// previously the only rejection path with no log, which made a looping
|
||||
// "Invalid token" agent impossible to diagnose without reading source.
|
||||
log.Warn().
|
||||
Str("agent_id", requestedID).
|
||||
Str("hostname", requestedHost).
|
||||
Msg("Agent exec token not recognized by this server — re-run the agent installer to re-enroll this agent")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user