the correction gesture is step-up gated after all (V-630)
Trace ids are sequential integers and the label table is the one thing the routing heads will be fitted on, so an ungated POST let anyone past the transport gate mislabel turns the owner never touched. The cost argument for leaving it open does not hold: he tapped to send the turn he is correcting, so the session is already up when the buttons appear.
This commit is contained in:
+9
-5
@@ -131,11 +131,12 @@ func handleChatAPI(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, ses
|
|||||||
// optionally the intent it should have been. An unstated target is accepted,
|
// optionally the intent it should have been. An unstated target is accepted,
|
||||||
// because a turn marked wrong with no target is still a usable negative.
|
// because a turn marked wrong with no target is still a usable negative.
|
||||||
//
|
//
|
||||||
// Not step-up gated, unlike POST /api/chat. Writing a label reaches no router,
|
// Step-up gated like POST /api/chat, and that costs the gesture nothing: he
|
||||||
// no model and no act path; it writes one row nothing executes from. Gating it
|
// tapped to send the turn he is now correcting, so the session is already up.
|
||||||
// would price the gesture out of being used, which is the one thing that makes
|
// It is gated because trace ids are sequential integers and this writes the one
|
||||||
// it worthless.
|
// table the routing heads (V-546) will be fitted on. A caller who can guess an
|
||||||
func handleCorrectAPI(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
// id could otherwise mislabel turns he never corrected.
|
||||||
|
func handleCorrectAPI(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, session *webauthn.PasskeySession, requireStepUp bool) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
@@ -143,6 +144,9 @@ func handleCorrectAPI(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI)
|
|||||||
if !requireCore(w, core, "correct") {
|
if !requireCore(w, core, "correct") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !stepUpGate(w, session, requireStepUp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
id, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("trace_id")), 10, 64)
|
id, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("trace_id")), 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
http.Error(w, "trace_id required", http.StatusBadRequest)
|
http.Error(w, "trace_id required", http.StatusBadRequest)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestCorrectAPIWithTarget(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleCorrectAPI(rr, postCorrect(url.Values{
|
handleCorrectAPI(rr, postCorrect(url.Values{
|
||||||
"trace_id": {"42"}, "should_be": {"fact"}, "q": {"поужинал"}, "rep": {"поняла"},
|
"trace_id": {"42"}, "should_be": {"fact"}, "q": {"поужинал"}, "rep": {"поняла"},
|
||||||
}), core)
|
}), core, stepUpSession(), false)
|
||||||
|
|
||||||
if rr.Code != http.StatusSeeOther {
|
if rr.Code != http.StatusSeeOther {
|
||||||
t.Fatalf("status %d, want 303; body=%s", rr.Code, rr.Body.String())
|
t.Fatalf("status %d, want 303; body=%s", rr.Code, rr.Body.String())
|
||||||
@@ -58,7 +58,7 @@ func TestCorrectAPIWithTarget(t *testing.T) {
|
|||||||
func TestCorrectAPIWithNoTarget(t *testing.T) {
|
func TestCorrectAPIWithNoTarget(t *testing.T) {
|
||||||
core := &correctCore{}
|
core := &correctCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"7"}}), core)
|
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"7"}}), core, stepUpSession(), false)
|
||||||
|
|
||||||
if rr.Code != http.StatusSeeOther {
|
if rr.Code != http.StatusSeeOther {
|
||||||
t.Fatalf("status %d, want 303", rr.Code)
|
t.Fatalf("status %d, want 303", rr.Code)
|
||||||
@@ -76,7 +76,7 @@ func TestCorrectAPIWithNoTarget(t *testing.T) {
|
|||||||
func TestCorrectAPIRejectsUnknownTarget(t *testing.T) {
|
func TestCorrectAPIRejectsUnknownTarget(t *testing.T) {
|
||||||
core := &correctCore{}
|
core := &correctCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"7"}, "should_be": {"погода"}}), core)
|
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"7"}, "should_be": {"погода"}}), core, stepUpSession(), false)
|
||||||
|
|
||||||
if rr.Code != http.StatusBadRequest {
|
if rr.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("status %d, want 400", rr.Code)
|
t.Fatalf("status %d, want 400", rr.Code)
|
||||||
@@ -90,7 +90,7 @@ func TestCorrectAPINeedsTraceID(t *testing.T) {
|
|||||||
for _, form := range []url.Values{{}, {"trace_id": {"0"}}, {"trace_id": {"nope"}}} {
|
for _, form := range []url.Values{{}, {"trace_id": {"0"}}, {"trace_id": {"nope"}}} {
|
||||||
core := &correctCore{}
|
core := &correctCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleCorrectAPI(rr, postCorrect(form), core)
|
handleCorrectAPI(rr, postCorrect(form), core, stepUpSession(), false)
|
||||||
if rr.Code != http.StatusBadRequest {
|
if rr.Code != http.StatusBadRequest {
|
||||||
t.Errorf("form %v: status %d, want 400", form, rr.Code)
|
t.Errorf("form %v: status %d, want 400", form, rr.Code)
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ func TestCorrectAPINeedsTraceID(t *testing.T) {
|
|||||||
func TestCorrectAPIReportsFailure(t *testing.T) {
|
func TestCorrectAPIReportsFailure(t *testing.T) {
|
||||||
core := &correctCore{err: errors.New("disk is full")}
|
core := &correctCore{err: errors.New("disk is full")}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"9"}, "should_be": {"note"}}), core)
|
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"9"}, "should_be": {"note"}}), core, stepUpSession(), false)
|
||||||
if rr.Code != http.StatusBadGateway {
|
if rr.Code != http.StatusBadGateway {
|
||||||
t.Fatalf("status %d, want 502", rr.Code)
|
t.Fatalf("status %d, want 502", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ func TestCorrectAPIReportsFailure(t *testing.T) {
|
|||||||
func TestCorrectAPIExpiredTurn(t *testing.T) {
|
func TestCorrectAPIExpiredTurn(t *testing.T) {
|
||||||
core := &correctCore{err: ipc.ErrNoSuchTrace}
|
core := &correctCore{err: ipc.ErrNoSuchTrace}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"9"}, "should_be": {"note"}}), core)
|
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"9"}, "should_be": {"note"}}), core, stepUpSession(), false)
|
||||||
if rr.Code != http.StatusNotFound {
|
if rr.Code != http.StatusNotFound {
|
||||||
t.Fatalf("status %d, want 404", rr.Code)
|
t.Fatalf("status %d, want 404", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ func TestCorrectAPIExpiredTurn(t *testing.T) {
|
|||||||
|
|
||||||
func TestCorrectAPIPostOnly(t *testing.T) {
|
func TestCorrectAPIPostOnly(t *testing.T) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleCorrectAPI(rr, httptest.NewRequest(http.MethodGet, "/api/correct", nil), &correctCore{})
|
handleCorrectAPI(rr, httptest.NewRequest(http.MethodGet, "/api/correct", nil), &correctCore{}, stepUpSession(), false)
|
||||||
if rr.Code != http.StatusMethodNotAllowed {
|
if rr.Code != http.StatusMethodNotAllowed {
|
||||||
t.Fatalf("status %d, want 405", rr.Code)
|
t.Fatalf("status %d, want 405", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -144,3 +144,17 @@ func TestCorrectionTargetsAreTheSeven(t *testing.T) {
|
|||||||
t.Error("empty is not a target: it is the absence of one, handled separately")
|
t.Error("empty is not a target: it is the absence of one, handled separately")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trace ids are sequential, so a caller who cannot assert step-up must not be
|
||||||
|
// able to label a turn the owner never corrected.
|
||||||
|
func TestCorrectAPINeedsStepUp(t *testing.T) {
|
||||||
|
core := &correctCore{}
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handleCorrectAPI(rr, postCorrect(url.Values{"trace_id": {"9"}, "should_be": {"note"}}), core, nil, true)
|
||||||
|
if rr.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("status %d, want 403", rr.Code)
|
||||||
|
}
|
||||||
|
if core.called {
|
||||||
|
t.Error("wrote a label with no step-up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-7
@@ -210,13 +210,7 @@ func main() {
|
|||||||
mux.HandleFunc("/routines", gatedPage(handleRoutines))
|
mux.HandleFunc("/routines", gatedPage(handleRoutines))
|
||||||
mux.HandleFunc("/api/chat", gatedPage(handleChatAPI))
|
mux.HandleFunc("/api/chat", gatedPage(handleChatAPI))
|
||||||
mux.HandleFunc("/api/revert", gatedPage(handleRevert))
|
mux.HandleFunc("/api/revert", gatedPage(handleRevert))
|
||||||
// POST /api/correct is deliberately NOT on the step-up list (V-630). It
|
mux.HandleFunc("/api/correct", gatedPage(handleCorrectAPI))
|
||||||
// reaches no router, no model and no act path: it writes one label row that
|
|
||||||
// nothing executes from. A correction that costs a passkey tap is a
|
|
||||||
// correction the owner does not make, and then the table stays empty.
|
|
||||||
mux.HandleFunc("/api/correct", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
handleCorrectAPI(w, r, core)
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/models", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/models", func(w http.ResponseWriter, r *http.Request) {
|
||||||
handleModels(w, r, core, swapConn, stepUpSession, *requireStepUp)
|
handleModels(w, r, core, swapConn, stepUpSession, *requireStepUp)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user