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
+5 -13
View File
@@ -16,8 +16,8 @@ import (
"strings"
"sync"
"time"
"unicode"
"cmbuyer/admin/internal/deviceauth"
core "cmbuyer/admin/internal/evidence"
)
@@ -152,7 +152,7 @@ func (store *Store) Discard(staged core.StagedFile) {
}
}
func (store *Store) Commit(ctx context.Context, principal core.DevicePrincipal, metadata core.UploadMetadata, staged core.StagedFile) (core.Asset, bool, error) {
func (store *Store) Commit(ctx context.Context, principal deviceauth.Principal, metadata core.UploadMetadata, staged core.StagedFile) (core.Asset, bool, error) {
if !store.isStagedPath(staged.Path) || !validPrincipal(principal) || !validMetadata(metadata) || metadata.SHA256 != staged.SHA256 || staged.ContentType != core.PNGContentType || staged.ByteSize < 1 || staged.ByteSize > core.MaxFileBytes || staged.Width < 1 || staged.Height < 1 || staged.Width > core.MaxImageSide || staged.Height > core.MaxImageSide || int64(staged.Width)*int64(staged.Height) > core.MaxImagePixels {
store.Discard(staged)
return core.Asset{}, false, core.ErrInvalid
@@ -447,16 +447,8 @@ func validMetadata(metadata core.UploadMetadata) bool {
return validUUID(metadata.UploadKey) && validUUID(metadata.TaskID) && validUUID(metadata.AttemptID) && metadata.Kind == core.KindSKUPanelGate1 && metadata.PrivacyTier == core.PrivacyInternalRaw && validSHA256(metadata.SHA256) && !metadata.CapturedAt.IsZero() && metadata.CapturedAt.Location() == time.UTC
}
func validPrincipal(principal core.DevicePrincipal) bool {
if principal.ID == "" || strings.TrimSpace(principal.ID) != principal.ID || len(principal.ID) > 128 {
return false
}
for _, character := range principal.ID {
if unicode.IsControl(character) {
return false
}
}
return true
func validPrincipal(principal deviceauth.Principal) bool {
return deviceauth.ValidDeviceID(principal.ID)
}
func validSHA256(value string) bool {
@@ -548,6 +540,6 @@ func scanAsset(row rowScanner) (core.Asset, bool, error) {
return asset, true, nil
}
func sameUpload(asset core.Asset, principal core.DevicePrincipal, metadata core.UploadMetadata, staged core.StagedFile) bool {
func sameUpload(asset core.Asset, principal deviceauth.Principal, metadata core.UploadMetadata, staged core.StagedFile) bool {
return asset.TaskID == metadata.TaskID && asset.AttemptID == metadata.AttemptID && asset.Kind == metadata.Kind && asset.PrivacyTier == metadata.PrivacyTier && asset.SHA256 == metadata.SHA256 && asset.ByteSize == staged.ByteSize && asset.ContentType == staged.ContentType && asset.Width == staged.Width && asset.Height == staged.Height && asset.UploadedByDeviceID == principal.ID && asset.CapturedAt.Equal(metadata.CapturedAt)
}
+11 -9
View File
@@ -19,6 +19,7 @@ import (
"testing"
"time"
"cmbuyer/admin/internal/deviceauth"
core "cmbuyer/admin/internal/evidence"
"cmbuyer/admin/internal/migrations"
"cmbuyer/admin/internal/storage/sqlite"
@@ -29,6 +30,7 @@ const (
testAuthID = "23c9f507-7473-4fa6-8d71-8786c34c6301"
testAttemptID = "33c9f507-7473-4fa6-8d71-8786c34c6301"
testUploadKey = "43c9f507-7473-4fa6-8d71-8786c34c6301"
testDeviceID = "53c9f507-7473-4fa6-8d71-8786c34c6301"
)
func TestStageCommitReplayAndOpen(t *testing.T) {
@@ -37,7 +39,7 @@ func TestStageCommitReplayAndOpen(t *testing.T) {
pngBytes := makePNG(t, 8, 6)
hash := sha256Hex(pngBytes)
metadata := core.UploadMetadata{UploadKey: testUploadKey, TaskID: testTaskID, AttemptID: testAttemptID, Kind: core.KindSKUPanelGate1, PrivacyTier: core.PrivacyInternalRaw, SHA256: hash, CapturedAt: time.Date(2026, 8, 4, 1, 2, 3, 0, time.UTC)}
principal := core.DevicePrincipal{ID: "device-one"}
principal := deviceauth.Principal{ID: testDeviceID}
staged, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
if err != nil {
@@ -101,7 +103,7 @@ func TestConcurrentReplayCreatesOneAsset(t *testing.T) {
for index := range staged {
go func(index int) {
defer wait.Done()
assets[index], replays[index], errorsSeen[index] = store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, metadata, staged[index])
assets[index], replays[index], errorsSeen[index] = store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, metadata, staged[index])
}(index)
}
wait.Wait()
@@ -206,7 +208,7 @@ func TestCommitSyncsShardAndRenameBeforeDatabaseWrite(t *testing.T) {
return os.Rename(oldPath, newPath)
}
if _, replayed, err := store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, metadata, staged); err != nil || replayed {
if _, replayed, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, metadata, staged); err != nil || replayed {
t.Fatalf("Commit = replayed %t, err %v", replayed, err)
}
if got, want := strings.Join(events, ","), "sync-root,sync-root,sync-file,rename,sync-shard"; got != want {
@@ -241,7 +243,7 @@ func TestCommitDirectorySyncFailuresNeverWriteDatabase(t *testing.T) {
return syncDirectory(path)
}
if _, _, err := store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, testMetadata(hash), staged); !errors.Is(err, injected) {
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), staged); !errors.Is(err, injected) {
t.Fatalf("Commit error = %v, want injected sync failure", err)
}
if calls != failAt {
@@ -275,7 +277,7 @@ func TestCommitRetriesShardParentSyncAfterPriorFailure(t *testing.T) {
}
injected := errors.New("injected first shard parent sync failure")
store.syncDirectory = func(string) error { return injected }
if _, _, err := store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, testMetadata(hash), firstStage); !errors.Is(err, injected) {
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), firstStage); !errors.Is(err, injected) {
t.Fatalf("first Commit error = %v", err)
}
if info, err := os.Stat(filepath.Dir(finalPath)); err != nil || !info.IsDir() {
@@ -292,7 +294,7 @@ func TestCommitRetriesShardParentSyncAfterPriorFailure(t *testing.T) {
paths = append(paths, path)
return syncDirectory(path)
}
if _, replayed, err := store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, testMetadata(hash), secondStage); err != nil || replayed {
if _, replayed, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), secondStage); err != nil || replayed {
t.Fatalf("retry Commit = replayed %t, err %v", replayed, err)
}
if len(paths) != 2 || paths[0] != store.root || paths[1] != filepath.Dir(finalPath) {
@@ -323,7 +325,7 @@ func TestCommitPublicationFailuresCleanTempAndNeverWriteDatabase(t *testing.T) {
} else {
store.renameFile = func(string, string) error { return injected }
}
if _, _, err := store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, testMetadata(hash), staged); !errors.Is(err, injected) {
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), staged); !errors.Is(err, injected) {
t.Fatalf("Commit error = %v, want injected publication failure", err)
}
if _, err := os.Stat(finalPath); !errors.Is(err, os.ErrNotExist) {
@@ -377,7 +379,7 @@ func TestCommitDatabaseFailuresAfterDurableRenameLeaveOnlyOrphan(t *testing.T) {
return syncDirectory(path)
}
if _, _, err := store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, testMetadata(hash), staged); err == nil {
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), staged); err == nil {
t.Fatal("Commit unexpectedly succeeded")
}
if syncCalls != 3 {
@@ -436,7 +438,7 @@ func TestCommitRequiresAttemptOwnedByTaskAndLowercaseHash(t *testing.T) {
}
metadata := base
mutate(&metadata)
if _, _, err := store.Commit(context.Background(), core.DevicePrincipal{ID: "device"}, metadata, staged); !errors.Is(err, core.ErrInvalid) {
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, metadata, staged); !errors.Is(err, core.ErrInvalid) {
t.Fatalf("Commit error = %v", err)
}
})