Allow legacy report-only agent tokens to fetch their own config (#1254)

Back-port v5 fix 04a828264 to v6. canReadConfig now accepts
ScopeAgentReport (legacy host-agent:report) in addition to the config/
manage/settings scopes; resolveConfigAgent already restricts a
report-only token to the host it is bound to, so it can only read its
own config. Without this, a v5 agent upgraded to a v6 server while still
holding a report-only token gets HTTP 403 on config fetch and breaks.

Retargets TestHandleConfigMissingConfigScope to monitoring:read and adds
TestHandleConfigAllowsBoundAgentReportScope as a #1254 regression guard.
This commit is contained in:
rcourtman 2026-06-04 08:39:31 +01:00
parent 545a6f22a0
commit d9da9b18f2
2 changed files with 45 additions and 2 deletions

View file

@ -350,6 +350,11 @@ func (h *UnifiedAgentHandlers) canReadConfig(record *config.APITokenRecord) bool
return true
}
return record.HasScope(config.ScopeAgentConfigRead) ||
// Older host/docker-agent tokens may only carry agent:report (legacy
// host-agent:report). Allow them to keep fetching their own config via
// token binding — resolveConfigAgent below restricts a report-only token
// to the host it is bound to.
record.HasScope(config.ScopeAgentReport) ||
record.HasScope(config.ScopeAgentManage) ||
record.HasScope(config.ScopeSettingsWrite)
}

View file

@ -340,8 +340,10 @@ func TestHandleConfigMissingConfigScope(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/agents/agent/"+hostID+"/config", nil)
attachAPITokenRecord(req, &config.APITokenRecord{
ID: "token-other",
Scopes: []string{config.ScopeAgentReport},
ID: "token-other",
// monitoring:read genuinely lacks config-read access (agent:report is
// now accepted via token binding, so it would not exercise this path).
Scopes: []string{config.ScopeMonitoringRead},
})
rec := httptest.NewRecorder()
@ -352,6 +354,42 @@ func TestHandleConfigMissingConfigScope(t *testing.T) {
}
}
// TestHandleConfigAllowsBoundAgentReportScope verifies that a legacy
// report-only token (agent:report / host-agent:report) can still fetch its own
// config via token binding — it resolves to the host bound to the token, not
// the requested agentID. Regression guard for #1254.
func TestHandleConfigAllowsBoundAgentReportScope(t *testing.T) {
t.Parallel()
handler := newUnifiedAgentHandlerForTests(t, models.Host{
ID: "host-1",
TokenID: "token-expected",
})
req := httptest.NewRequest(http.MethodGet, "/api/agents/agent/other-host/config", nil)
attachAPITokenRecord(req, &config.APITokenRecord{
ID: "token-expected",
Scopes: []string{config.ScopeAgentReport},
})
rec := httptest.NewRecorder()
handler.HandleConfig(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp struct {
AgentID string `json:"agentId"`
}
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if resp.AgentID != "host-1" {
t.Fatalf("expected agent id %q, got %q", "host-1", resp.AgentID)
}
}
func TestHandleConfigUsesTokenBinding(t *testing.T) {
t.Parallel()