mavweb: gate /api/revert behind passkey step-up
RevertFact voids the latest fact for a key — a store mutation — but /api/revert had no step-up gate, while POST /tools required L3. Close the inconsistency: thread the same *webauthn.PasskeySession into handleRevert and reject with 403 when a configured session isn't asserted. nil session (WebAuthn unconfigured) keeps prior behavior — transport-level auth only. Tests: un-asserted session → 403 and RevertFact not called; asserted → 200. The RevertFact mock now records its key so the gate assertion is meaningful. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -137,7 +137,8 @@ func (f *fakeCore) RecentNotes(_ context.Context, _ int) ([]ipc.Note, error) {
|
||||
return f.notes, nil
|
||||
}
|
||||
|
||||
func (f *fakeCore) RevertFact(_ context.Context, _ string) (int64, error) {
|
||||
func (f *fakeCore) RevertFact(_ context.Context, key string) (int64, error) {
|
||||
f.revertKey = key
|
||||
if f.revertErr != nil {
|
||||
return 0, f.revertErr
|
||||
}
|
||||
@@ -756,7 +757,7 @@ func TestHandleRevert(t *testing.T) {
|
||||
|
||||
t.Run("GET returns 405", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
handleRevert(rr, httptest.NewRequest(http.MethodGet, "/api/revert", nil), &fakeCore{})
|
||||
handleRevert(rr, httptest.NewRequest(http.MethodGet, "/api/revert", nil), &fakeCore{}, nil)
|
||||
if rr.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("status = %d, want 405", rr.Code)
|
||||
}
|
||||
@@ -764,7 +765,7 @@ func TestHandleRevert(t *testing.T) {
|
||||
|
||||
t.Run("nil core returns 503", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
handleRevert(rr, httptest.NewRequest(http.MethodPost, "/api/revert", nil), nil)
|
||||
handleRevert(rr, httptest.NewRequest(http.MethodPost, "/api/revert", nil), nil, nil)
|
||||
if rr.Code != http.StatusServiceUnavailable {
|
||||
t.Errorf("status = %d, want 503", rr.Code)
|
||||
}
|
||||
@@ -775,7 +776,7 @@ func TestHandleRevert(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key="))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
handleRevert(rr, req, core)
|
||||
handleRevert(rr, req, core, nil)
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want 400", rr.Code)
|
||||
}
|
||||
@@ -786,7 +787,7 @@ func TestHandleRevert(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=missing"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
handleRevert(rr, req, core)
|
||||
handleRevert(rr, req, core, nil)
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Errorf("status = %d, want 404", rr.Code)
|
||||
}
|
||||
@@ -797,7 +798,7 @@ func TestHandleRevert(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=somekey"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
handleRevert(rr, req, core)
|
||||
handleRevert(rr, req, core, nil)
|
||||
if rr.Code != http.StatusBadGateway {
|
||||
t.Errorf("status = %d, want 502", rr.Code)
|
||||
}
|
||||
@@ -808,7 +809,7 @@ func TestHandleRevert(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=test-key"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
handleRevert(rr, req, core)
|
||||
handleRevert(rr, req, core, nil)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
@@ -823,6 +824,32 @@ func TestHandleRevert(t *testing.T) {
|
||||
t.Errorf("body missing new_id:42: %s", body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("configured but un-asserted session returns 403", func(t *testing.T) {
|
||||
core := &fakeCore{revertNewID: 7}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=k"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
// non-nil, never asserted ⇒ IsStepUp() false ⇒ gate closes.
|
||||
handleRevert(rr, req, core, webauthn.NewPasskeySession(5*time.Minute))
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Errorf("status = %d, want 403", rr.Code)
|
||||
}
|
||||
if core.revertKey != "" {
|
||||
t.Error("RevertFact called despite closed step-up gate")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("asserted session passes the gate", func(t *testing.T) {
|
||||
core := &fakeCore{revertNewID: 9}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=k"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
handleRevert(rr, req, core, stepUpSession())
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- handleTools ListTools error ---
|
||||
|
||||
Reference in New Issue
Block a user