Fix agent-token fallback to reject cross-org tokens and update security contract test

Two test regressions introduced when agent-report tokens were allowed as
fallback auth for /api/auto-register:

1. Org mismatch was not checked: a token belonging to org-a could authenticate
   a request whose context carried org-b. Add an explicit org consistency check
   before setting authenticated=true in the fallback path.

2. The security regression test assumed only setup tokens could authenticate
   auto-register. That contract has intentionally changed: agent-report tokens
   can now authenticate but are restricted to updating existing nodes (403 for
   new-node attempts). Update the test to assert the actual security boundary.
This commit is contained in:
rcourtman 2026-04-18 23:10:50 +01:00
parent 264c9377a2
commit 9bac3f421d
2 changed files with 21 additions and 11 deletions

View file

@ -718,12 +718,19 @@ func (h *ConfigHandlers) handleAutoRegister(w http.ResponseWriter, r *http.Reque
record, ok := cfg.ValidateAPIToken(apiToken)
config.Mu.Unlock()
if ok && record != nil && record.HasScope(config.ScopeAgentReport) {
authenticated = true
r = r.WithContext(context.WithValue(r.Context(), agentAutoRegContextKey{}, true))
log.Info().
Str("type", req.Type).
Str("host", req.Host).
Msg("Auto-register authenticated via agent API token (update-only mode)")
// Reject cross-org tokens: if the request context has an explicit
// non-default org, the token must belong to that same org.
requestOrgID := GetOrgID(r.Context())
orgMismatch := requestOrgID != "" && requestOrgID != "default" &&
record.OrgID != "" && record.OrgID != requestOrgID
if !orgMismatch {
authenticated = true
r = r.WithContext(context.WithValue(r.Context(), agentAutoRegContextKey{}, true))
log.Info().
Str("type", req.Type).
Str("host", req.Host).
Msg("Auto-register authenticated via agent API token (update-only mode)")
}
}
}
}

View file

@ -2065,16 +2065,19 @@ func TestAutoRegisterRejectsAgentTokenWithoutSetupToken(t *testing.T) {
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
router.configHandlers.SetConfig(cfg)
body := `{"type":"pve","host":"https://192.168.1.1:8006","tokenId":"test@pam!pulse","tokenValue":"secret"}`
// Agent-report tokens can authenticate auto-register but are restricted to
// updating existing nodes. Attempting to register a brand-new node must
// return 403, not succeed.
body := `{"type":"pve","host":"https://192.168.1.1:8006","tokenId":"pulse-monitor@pve!pulse-192-168-1-1","tokenValue":"secret","source":"agent","serverName":"newhost"}`
req := httptest.NewRequest(http.MethodPost, "/api/auto-register", strings.NewReader(body))
req.Header.Set("X-API-Token", rawToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401 when agent API token is provided without a Pulse setup token, got %d", rec.Code)
if rec.Code != http.StatusForbidden {
t.Fatalf("expected 403 when agent API token is used to register a new node, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "Pulse setup token required") {
t.Fatalf("expected setup-token requirement guidance, got %q", rec.Body.String())
if !strings.Contains(rec.Body.String(), "agent token auth permits token updates for existing nodes only") {
t.Fatalf("expected agent-token restriction message, got %q", rec.Body.String())
}
}