feat: add permanent deletion for revoked keys
DELETE /admin/api/keys/{id}/purge removes the key record; it requires the key to be revoked first and keeps historical logs. The keys page swaps the revoke button for a delete button once a key is revoked.
This commit is contained in:
@@ -203,6 +203,27 @@ func (s *Server) keys(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
func (s *Server) keyID(w http.ResponseWriter, r *http.Request) {
|
||||
id := strings.TrimPrefix(r.URL.Path, "/admin/api/keys/")
|
||||
if purgeID, ok := strings.CutSuffix(id, "/purge"); ok {
|
||||
if r.Method != "DELETE" {
|
||||
errJSON(w, 405, "method not allowed")
|
||||
return
|
||||
}
|
||||
var revoked bool
|
||||
if e := s.db.QueryRow("SELECT revoked FROM keys WHERE id=?", purgeID).Scan(&revoked); e == sql.ErrNoRows {
|
||||
errJSON(w, 404, "not found")
|
||||
return
|
||||
} else if e != nil {
|
||||
errJSON(w, 500, "database error")
|
||||
return
|
||||
}
|
||||
if !revoked {
|
||||
errJSON(w, 409, "revoke the key before deleting")
|
||||
return
|
||||
}
|
||||
res, e := s.db.Exec("DELETE FROM keys WHERE id=?", purgeID)
|
||||
mutation(w, res, e)
|
||||
return
|
||||
}
|
||||
if r.Method == "DELETE" {
|
||||
res, e := s.db.Exec("UPDATE keys SET enabled=0,revoked=1 WHERE id=?", id)
|
||||
mutation(w, res, e)
|
||||
|
||||
@@ -252,6 +252,56 @@ func TestKeyRevocationAndProxy(t *testing.T) {
|
||||
if n != 1 {
|
||||
t.Fatal(n)
|
||||
}
|
||||
w = call(s, "DELETE", "/admin/api/keys/"+id+"/purge", "", c)
|
||||
if w.Code != 200 {
|
||||
t.Fatal(w.Body.String())
|
||||
}
|
||||
s.db.QueryRow("SELECT count(*) FROM keys WHERE id=?", id).Scan(&n)
|
||||
if n != 0 {
|
||||
t.Fatal("purged key still stored", n)
|
||||
}
|
||||
w = call(s, "DELETE", "/admin/api/keys/"+id+"/purge", "", c)
|
||||
if w.Code != 404 {
|
||||
t.Fatal("purged key not reported missing", w.Code)
|
||||
}
|
||||
s.db.QueryRow("SELECT count(*) FROM logs").Scan(&n)
|
||||
if n != 1 {
|
||||
t.Fatal("purge removed history", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyPurgeRequiresRevocation(t *testing.T) {
|
||||
s := testServer(t)
|
||||
c := loginCookie(t, s)
|
||||
w := call(s, "POST", "/admin/api/keys", `{"name":"gone","all_channels":true}`, c)
|
||||
var key map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &key); err != nil || w.Code != 200 {
|
||||
t.Fatal(w.Code, w.Body.String(), err)
|
||||
}
|
||||
var id string
|
||||
if err := s.db.QueryRow("SELECT id FROM keys WHERE name='gone'").Scan(&id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w = call(s, "DELETE", "/admin/api/keys/"+id+"/purge", "", c); w.Code != 409 {
|
||||
t.Fatal("purged active key", w.Code, w.Body.String())
|
||||
}
|
||||
if w = call(s, "DELETE", "/admin/api/keys/"+id, "", c); w.Code != 200 {
|
||||
t.Fatal(w.Body.String())
|
||||
}
|
||||
if w = call(s, "DELETE", "/admin/api/keys/"+id+"/purge", "", c); w.Code != 200 {
|
||||
t.Fatal(w.Body.String())
|
||||
}
|
||||
w = call(s, "GET", "/admin/api/keys", "", c)
|
||||
if strings.Contains(w.Body.String(), `"gone"`) {
|
||||
t.Fatal("purged key still listed", w.Body.String())
|
||||
}
|
||||
r := httptest.NewRequest("GET", "/v1/models", nil)
|
||||
r.Header.Set("Authorization", "Bearer "+key["key"])
|
||||
w = httptest.NewRecorder()
|
||||
s.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatal("purged key still authenticates", w.Code)
|
||||
}
|
||||
}
|
||||
func TestStatsUnknownAndFilters(t *testing.T) {
|
||||
s := testServer(t)
|
||||
|
||||
Reference in New Issue
Block a user