Reject revoked legacy sync API keys

This commit is contained in:
mirivlad 2026-06-27 13:39:00 +08:00
parent fcf0a07fcc
commit 2e6b451c3d
2 changed files with 61 additions and 8 deletions

View File

@ -36,13 +36,25 @@ func (s *Server) requireAuth(w http.ResponseWriter, r *http.Request) (deviceID,
s.db.Exec("UPDATE server_devices SET last_seen=? WHERE id=?", time.Now().UTC().Format(time.RFC3339), deviceIDVal.String) s.db.Exec("UPDATE server_devices SET last_seen=? WHERE id=?", time.Now().UTC().Format(time.RFC3339), deviceIDVal.String)
return deviceIDVal.String, userIDVal.String, true return deviceIDVal.String, userIDVal.String, true
} }
var count int err = s.db.QueryRow("SELECT id, user_id, revoked_at FROM server_devices WHERE api_key=?", key).Scan(&deviceIDVal, &userIDVal, &revokedAt)
err = s.db.QueryRow("SELECT COUNT(*) FROM server_devices WHERE api_key=?", key).Scan(&count) if err != nil {
if err != nil || count == 0 {
jsonErr(w, 401, "invalid API key") jsonErr(w, 401, "invalid API key")
return "", "", false return "", "", false
} }
return "", "", true if revokedAt.Valid && revokedAt.String != "" {
jsonErr(w, 401, "device revoked")
return "", "", false
}
if userIDVal.Valid && userIDVal.String != "" {
var blocked int
s.db.QueryRow("SELECT blocked FROM server_users WHERE id=?", userIDVal.String).Scan(&blocked)
if blocked != 0 {
jsonErr(w, 403, "user blocked")
return "", "", false
}
}
s.db.Exec("UPDATE server_devices SET last_seen=? WHERE id=?", time.Now().UTC().Format(time.RFC3339), deviceIDVal.String)
return deviceIDVal.String, userIDVal.String, true
} }
func (s *Server) requireAdmin(w http.ResponseWriter, r *http.Request) bool { func (s *Server) requireAdmin(w http.ResponseWriter, r *http.Request) bool {

View File

@ -132,7 +132,51 @@ func TestSyncPushPullStoresSequencedOps(t *testing.T) {
} }
} }
func TestRevokedLegacyAPIKeyCannotPushOrPull(t *testing.T) {
dir := t.TempDir()
s, err := NewServer(filepath.Join(dir, "test.db"), filepath.Join(dir, "data"), &Config{Port: 47732})
if err != nil {
t.Fatalf("NewServer: %v", err)
}
defer s.Close()
s.SetupRoutes()
now := time.Now().UTC().Format(time.RFC3339)
if _, err := s.db.Exec(
"INSERT INTO server_devices (id, name, api_key, last_seen, revoked_at, created_at) VALUES (?, ?, ?, ?, ?, ?)",
"device-revoked", "Revoked Device", "revoked-key", now, now, now,
); err != nil {
t.Fatalf("insert device: %v", err)
}
ts := httptest.NewServer(s.mux)
defer ts.Close()
pushStatus, pushResp := postJSONStatus(t, ts.URL+"/api/v1/sync/push", "revoked-key", map[string]interface{}{
"device_id": "device-revoked",
"ops": []map[string]interface{}{},
})
if pushStatus != http.StatusUnauthorized || pushResp["error"] != "device revoked" {
t.Fatalf("push status=%d resp=%#v, want 401 device revoked", pushStatus, pushResp)
}
pullStatus, pullResp := postJSONStatus(t, ts.URL+"/api/v1/sync/pull", "revoked-key", map[string]interface{}{
"since_sequence": 0,
})
if pullStatus != http.StatusUnauthorized || pullResp["error"] != "device revoked" {
t.Fatalf("pull status=%d resp=%#v, want 401 device revoked", pullStatus, pullResp)
}
}
func postJSON(t *testing.T, url, token string, body interface{}) map[string]interface{} { func postJSON(t *testing.T, url, token string, body interface{}) map[string]interface{} {
t.Helper()
status, out := postJSONStatus(t, url, token, body)
if status != http.StatusOK {
t.Fatalf("post %s status = %d", url, status)
}
return out
}
func postJSONStatus(t *testing.T, url, token string, body interface{}) (int, map[string]interface{}) {
t.Helper() t.Helper()
var b bytes.Buffer var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(body); err != nil { if err := json.NewEncoder(&b).Encode(body); err != nil {
@ -149,12 +193,9 @@ func postJSON(t *testing.T, url, token string, body interface{}) map[string]inte
t.Fatalf("post %s: %v", url, err) t.Fatalf("post %s: %v", url, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("post %s status = %d", url, resp.StatusCode)
}
var out map[string]interface{} var out map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode response: %v", err) t.Fatalf("decode response: %v", err)
} }
return out return resp.StatusCode, out
} }