feat(admin): add device credential isolation

This commit is contained in:
QiuSW
2026-08-04 20:57:33 +08:00
parent 41881e81f3
commit 66355a7f89
20 changed files with 1745 additions and 87 deletions
+84 -4
View File
@@ -26,19 +26,27 @@ func TestUpDownAndIdempotence(t *testing.T) {
if err := migrations.Up(context, database, directory); err != nil {
t.Fatalf("apply migrations: %v", err)
}
assertVersion(t, database, 3)
assertVersion(t, database, 4)
assertTableExists(t, database, "tasks", true)
assertTableExists(t, database, "spec_trials", false)
assertTableExists(t, database, "order_authorizations", true)
assertTableExists(t, database, "purchase_attempts", true)
assertTableExists(t, database, "order_submissions", true)
assertTableExists(t, database, "evidence_assets", true)
assertTableExists(t, database, "device_credentials", true)
assertTableExists(t, database, "single_pass_upgrade_guard", false)
if err := migrations.Up(context, database, directory); err != nil {
t.Fatalf("reapply migrations: %v", err)
}
assertVersion(t, database, 4)
if err := migrations.Down(context, database, directory); err != nil {
t.Fatalf("roll back device credential migration: %v", err)
}
assertVersion(t, database, 3)
assertTableExists(t, database, "device_credentials", false)
assertTableExists(t, database, "evidence_assets", true)
if err := migrations.Down(context, database, directory); err != nil {
t.Fatalf("roll back evidence migration: %v", err)
@@ -57,7 +65,7 @@ func TestUpDownAndIdempotence(t *testing.T) {
if err := migrations.Up(context, database, directory); err != nil {
t.Fatalf("reapply v2 after rollback: %v", err)
}
assertVersion(t, database, 3)
assertVersion(t, database, 4)
}
func TestUpgradePreservesManualDraftLosslessly(t *testing.T) {
@@ -76,7 +84,7 @@ func TestUpgradePreservesManualDraftLosslessly(t *testing.T) {
if err := migrations.Up(context.Background(), database, migrationDirectory(t)); err != nil {
t.Fatalf("upgrade v1 draft: %v", err)
}
assertVersion(t, database, 3)
assertVersion(t, database, 4)
var got struct {
id, source, sourceRef, title, goodsID, color, size, maxPrice, assetID, status, created, updated string
quantity, version int
@@ -263,6 +271,9 @@ func TestEvidenceSchemaConstraintsAndDowngradeGuard(t *testing.T) {
})
}
if err := migrations.Down(context.Background(), database, migrationDirectory(t)); err != nil {
t.Fatalf("roll back empty device credential migration: %v", err)
}
if err := migrations.Down(context.Background(), database, migrationDirectory(t)); err == nil {
t.Fatal("evidence-bearing schema downgraded successfully")
}
@@ -274,6 +285,75 @@ func TestEvidenceSchemaConstraintsAndDowngradeGuard(t *testing.T) {
}
}
func TestDeviceCredentialSchemaConstraintsAndDowngradeGuard(t *testing.T) {
database := openTestDatabase(t)
if err := migrations.Up(context.Background(), database, migrationDirectory(t)); err != nil {
t.Fatalf("apply migrations: %v", err)
}
deviceID := "13c9f507-7473-4fa6-8d71-8786c34c6301"
hash := make([]byte, 32)
for index := range hash {
hash[index] = byte(index + 1)
}
insert := `INSERT INTO device_credentials (device_id, display_name, token_sha256, status, created_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?)`
valid := []any{deviceID, "采购工具一号", hash, "ACTIVE", migrationTime, nil}
if _, err := database.Exec(insert, valid...); err != nil {
t.Fatalf("insert valid credential: %v", err)
}
for name, mutate := range map[string]func([]any){
"uppercase uuid": func(values []any) { values[0], values[2] = strings.ToUpper(deviceID), append([]byte(nil), hash...) },
"wrong uuid version": func(values []any) {
values[0], values[2] = "23c9f507-7473-3fa6-8d71-8786c34c6301", append([]byte(nil), hash...)
},
"blank display name": func(values []any) {
values[0], values[1], values[2] = "33c9f507-7473-4fa6-8d71-8786c34c6301", "", append([]byte(nil), hash...)
},
"padded display name": func(values []any) {
values[0], values[1], values[2] = "43c9f507-7473-4fa6-8d71-8786c34c6301", " padded", append([]byte(nil), hash...)
},
"text hash": func(values []any) {
values[0], values[2] = "53c9f507-7473-4fa6-8d71-8786c34c6301", strings.Repeat("a", 32)
},
"short blob hash": func(values []any) { values[0], values[2] = "63c9f507-7473-4fa6-8d71-8786c34c6301", make([]byte, 31) },
"unknown status": func(values []any) {
values[0], values[2], values[3] = "73c9f507-7473-4fa6-8d71-8786c34c6301", append([]byte(nil), hash...), "UNKNOWN"
},
"active with revoke time": func(values []any) {
values[0], values[2], values[5] = "83c9f507-7473-4fa6-8d71-8786c34c6301", append([]byte(nil), hash...), migrationTime
},
"revoked without time": func(values []any) {
values[0], values[2], values[3] = "93c9f507-7473-4fa6-8d71-8786c34c6301", append([]byte(nil), hash...), "REVOKED"
},
"revoke before creation": func(values []any) {
values[0], values[2], values[3], values[4], values[5] = "b3c9f507-7473-4fa6-8d71-8786c34c6301", append([]byte(nil), hash...), "REVOKED", "2026-08-04T01:00:00Z", "2026-08-04T00:00:00Z"
},
"non UTC created time": func(values []any) {
values[0], values[2], values[4] = "a3c9f507-7473-4fa6-8d71-8786c34c6301", append([]byte(nil), hash...), "2026-08-04T08:00:00+08:00"
},
} {
t.Run(name, func(t *testing.T) {
values := append([]any(nil), valid...)
mutate(values)
if bytesValue, ok := values[2].([]byte); ok && len(bytesValue) == 32 {
bytesValue[0]++
}
if _, err := database.Exec(insert, values...); err == nil {
t.Fatal("invalid device credential row succeeded")
}
})
}
if err := migrations.Down(context.Background(), database, migrationDirectory(t)); err == nil {
t.Fatal("credential-bearing schema downgraded successfully")
}
assertVersion(t, database, 4)
assertTableExists(t, database, "device_credentials", true)
var count int
if err := database.QueryRow(`SELECT COUNT(*) FROM device_credentials`).Scan(&count); err != nil || count != 1 {
t.Fatalf("credentials after rejected downgrade = %d, err=%v", count, err)
}
}
func TestDowngradeRejectsV2BusinessDataAtomically(t *testing.T) {
tests := []struct {
name string
@@ -456,7 +536,7 @@ func assertColumnType(t *testing.T, database *sql.DB, table, column, want string
}
func TestMigrationsDoNotDisableForeignKeys(t *testing.T) {
for _, name := range []string{"00002_single_pass_model.sql", "00003_evidence_assets.sql"} {
for _, name := range []string{"00002_single_pass_model.sql", "00003_evidence_assets.sql", "00004_device_credentials.sql"} {
contents, err := os.ReadFile(filepath.Join(migrationDirectory(t), name))
if err != nil {
t.Fatalf("read %s: %v", name, err)