fix(security): API keys only deliver data; one auth check everywhere

Until API keys get scopes they have exactly one right: POST /api/v1/inbox
(thought or task via target) and the webhook ingest; every other endpoint
answers 403 for a key. Middleware and webhook ingest share one token check
(revocation list, token_version, active account), closing FLAW-08.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 07:39:29 +01:00
1 parent 62463bc6fd
commit 300fd6a7be
4 files changed
+99 -49

No files matched your search

+43
View File
@@ -1406,3 +1406,46 @@ func TestLegalTextsArePublic(t *testing.T) {
t.Fatalf("privacy without text should be 404, got %d", rec.Code)
}
}
// API keys have exactly one right until scopes exist: delivering data. Revoked sessions are refused
// everywhere, the webhook ingest included.
func TestAPIKeysOnlyDeliverData(t *testing.T) {
srv, store, token := setupTestServer(t)
router := srv.Router()
claims, _ := auth.ValidateToken(testJWTSecret, token)
key, err := store.CreateAPIKey(claims.UserID, "Home Assistant")
if err != nil {
t.Fatalf("CreateAPIKey: %v", err)
}
do := func(method, path, auth, body string) int {
req := httptest.NewRequest(method, path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
if strings.HasPrefix(auth, "mindsift_sec_") {
req.Header.Set("X-API-Key", auth)
} else {
req.Header.Set("Authorization", "Bearer "+auth)
}
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
return rec.Code
}
if code := do(http.MethodPost, "/api/v1/inbox", key.RawKey, `{"content":"Waschmaschine fertig"}`); code >= 300 {
t.Fatalf("API key must be able to post into the inbox, got %d", code)
}
if code := do(http.MethodPost, "/api/v1/inbox", key.RawKey, `{"content":"Filter wechseln","target":"task"}`); code >= 300 {
t.Fatalf("API key must be able to create a task via the inbox, got %d", code)
}
if code := do(http.MethodGet, "/api/v1/documents", key.RawKey, ""); code != http.StatusForbidden {
t.Fatalf("API key must not read notes, got %d", code)
}
if code := do(http.MethodPost, "/api/v1/webhooks/ingest", key.RawKey, `{"content":"Rauchmelder"}`); code >= 300 {
t.Fatalf("API key must work on the webhook ingest, got %d", code)
}
// Logged-out session token: refused by the ingest as well (was FLAW-08)
if code := do(http.MethodPost, "/api/v1/auth/logout", token, ""); code >= 300 {
t.Fatalf("logout: %d", code)
}
if code := do(http.MethodPost, "/api/v1/webhooks/ingest", token, `{"content":"x"}`); code != http.StatusUnauthorized {
t.Fatalf("revoked token must be refused by the ingest, got %d", code)
}
}