merge fix/token-backfill-null: legacy NULL token_plain backfill (V1.0.1)
This commit is contained in:
@@ -83,11 +83,16 @@ func (a *AgentController) findAgent(token string) (*model.AgentNode, bool) {
|
||||
// Item 19 backfill: the token has just passed the hash check, so for
|
||||
// rows created before plaintext storage existed, adopt the presented
|
||||
// token as the recoverable copy. The guarded update only fires while
|
||||
// token_plain is empty — an authenticated request can never overwrite
|
||||
// an already-stored token (only issuance writes a fresh one).
|
||||
// token_plain is unset — an authenticated request can never overwrite
|
||||
// an already-stored token (only issuance writes a fresh one). "Unset"
|
||||
// must cover SQL NULL as well as '': AutoMigrate added the column to
|
||||
// pre-existing rows as NULL, and NULL = '' is never true, so an
|
||||
// equality-only guard silently matched zero legacy rows in
|
||||
// production (the GORM-scanned value reads as "" either way, which is
|
||||
// why the in-memory check above cannot distinguish them).
|
||||
if agent.TokenPlain == "" {
|
||||
if res := database.GetDB().Model(&model.AgentNode{}).
|
||||
Where("id = ? AND token_plain = ''", agent.Id).
|
||||
Where("id = ? AND (token_plain IS NULL OR token_plain = '')", agent.Id).
|
||||
Update("token_plain", token); res.Error == nil && res.RowsAffected > 0 {
|
||||
agent.TokenPlain = token
|
||||
}
|
||||
|
||||
@@ -123,6 +123,62 @@ func TestAgentTokenBackfillOnAuthenticatedRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Regression for the production failure of the first V1.0.0 deployment:
|
||||
// rows that existed before item 19 got token_plain from AutoMigrate as
|
||||
// SQL NULL, not ”, and the original guard `token_plain = ”` never
|
||||
// matches NULL — the backfill silently never fired on any legacy node.
|
||||
// Seed the row exactly as production has it (raw SQL, explicit NULL)
|
||||
// instead of through GORM, which would write ”.
|
||||
func TestAgentTokenBackfillFromNullLegacyRow(t *testing.T) {
|
||||
engine, node := setupBackfillTest(t)
|
||||
const token = "legacy-token-backfill-00000000000000000004"
|
||||
db := database.GetDB()
|
||||
if err := db.Exec(
|
||||
"INSERT INTO agent_nodes (token_hash, token_prefix, token_plain, name, node_id, last_seen, created_at, updated_at) VALUES (?, ?, NULL, ?, ?, 0, 0, 0)",
|
||||
hashAgentToken(token), token[:12], "backfill-node", node.Id,
|
||||
).Error; err != nil {
|
||||
t.Fatalf("seed NULL legacy row: %v", err)
|
||||
}
|
||||
var seeded model.AgentNode
|
||||
if err := db.Where("token_hash = ?", hashAgentToken(token)).First(&seeded).Error; err != nil {
|
||||
t.Fatalf("load seeded row: %v", err)
|
||||
}
|
||||
var nullRows int
|
||||
if err := db.Raw("SELECT COUNT(*) FROM agent_nodes WHERE id = ? AND token_plain IS NULL", seeded.Id).Scan(&nullRows).Error; err != nil {
|
||||
t.Fatalf("NULL check: %v", err)
|
||||
}
|
||||
if nullRows != 1 {
|
||||
t.Fatalf("seeded row is not SQL NULL as intended (null rows = %d)", nullRows)
|
||||
}
|
||||
|
||||
if code := heartbeatWith(engine, token); code != http.StatusOK {
|
||||
t.Fatalf("heartbeat with valid token: status %d", code)
|
||||
}
|
||||
if row := reloadAgent(t, seeded.Id); row.TokenPlain != token {
|
||||
t.Fatalf("token not backfilled from NULL legacy row: %q", row.TokenPlain)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/panel/api/agent/install/"+strconv.Itoa(node.Id), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
engine.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("install endpoint: status %d", rec.Code)
|
||||
}
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Obj struct {
|
||||
Token string `json:"token"`
|
||||
HasToken bool `json:"hasToken"`
|
||||
} `json:"obj"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("install response not JSON: %v", err)
|
||||
}
|
||||
if !resp.Success || !resp.Obj.HasToken || resp.Obj.Token != token {
|
||||
t.Fatalf("install endpoint did not return the backfilled token: %+v", resp.Obj)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTokenNoBackfillOnFailedAuth(t *testing.T) {
|
||||
engine, node := setupBackfillTest(t)
|
||||
const token = "legacy-token-backfill-00000000000000000002"
|
||||
|
||||
Reference in New Issue
Block a user