fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
This commit is contained in:
78
internal/services/channel_policy_concurrency_test.go
Normal file
78
internal/services/channel_policy_concurrency_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestChannelConfigCompareAndSwapRejectsStalePolicyAndSecretWrites(t *testing.T) {
|
||||
for _, action:=range []string{"editor", "secret-reset"} {
|
||||
t.Run(action,func(t *testing.T) {
|
||||
previousDB:=sqls.DB()
|
||||
db:=setupChannelServiceTestDB(t)
|
||||
t.Cleanup(func(){sqls.SetDB(previousDB)})
|
||||
agent:=createChannelServiceTestAgent(t,db,1001)
|
||||
channel,err:=ChannelService.CreateChannel(request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Policy channel",Status:int(enums.StatusOk),ConfigJSON:`{"requireSignedIdentity":false,"userTokenSecret":"test-old-secret"}`},channelServiceTestOperator())
|
||||
if err!=nil {t.Fatal(err)}
|
||||
var injected atomic.Bool
|
||||
callbackName:="test:concurrent_channel_policy"
|
||||
// Commit another writer after the stale service read, just before its SQL.
|
||||
// Preserve updated_at deliberately: bytewise JSON CAS must also catch this.
|
||||
if err:=db.Callback().Update().Before("gorm:update").Register(callbackName,func(tx *gorm.DB){
|
||||
if tx.Statement.Schema==nil || tx.Statement.Schema.Table!="t_channel" || !injected.CompareAndSwap(false,true){return}
|
||||
cfg,parseErr:=ChannelService.ParseWebChannelConfig(channel.ConfigJSON)
|
||||
if parseErr!=nil {tx.AddError(parseErr);return}
|
||||
cfg.RequireSignedIdentity=true
|
||||
cfg.UserTokenSecret="test-new-secret"
|
||||
encoded,encodeErr:=json.Marshal(cfg)
|
||||
if encodeErr!=nil {tx.AddError(encodeErr);return}
|
||||
if writeErr:=db.Session(&gorm.Session{NewDB:true}).Model(&models.Channel{}).Where("id = ?",channel.ID).Updates(map[string]any{"config_json":string(encoded),"updated_at":channel.UpdatedAt}).Error;writeErr!=nil{tx.AddError(writeErr)}
|
||||
});err!=nil{t.Fatal(err)}
|
||||
t.Cleanup(func(){_ = db.Callback().Update().Remove(callbackName)})
|
||||
switch action {
|
||||
case "editor": err=ChannelService.UpdateChannel(request.UpdateChannelRequest{ID:channel.ID,CreateChannelRequest:request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Old editor title",Status:int(enums.StatusOk),ConfigJSON:`{"title":"old editor"}`}},channelServiceTestOperator())
|
||||
case "secret-reset": var secret string;secret,err=ChannelService.ResetUserTokenSecret(channel.ID,channelServiceTestOperator());if secret!=""{t.Fatal("failed reset returned an uncommitted secret")}
|
||||
}
|
||||
if !injected.Load() || err==nil {t.Fatal("stale writer was not rejected")}
|
||||
current:=ChannelService.Get(channel.ID)
|
||||
cfg,parseErr:=ChannelService.ParseWebChannelConfig(current.ConfigJSON)
|
||||
if parseErr!=nil || !cfg.RequireSignedIdentity || cfg.UserTokenSecret!="test-new-secret" {t.Fatal("stale writer replaced the concurrent policy/secret")}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelConfigCompareAndSwapUsesExactBytes(t *testing.T) {
|
||||
db,channel:=setupIdentitySecurityDB(t)
|
||||
channel.ConfigJSON=`{"userTokenSecret":"CaseSensitive"}`
|
||||
if err:=db.Model(channel).Updates(map[string]any{"config_json":channel.ConfigJSON,"updated_at":time.Now()}).Error;err!=nil{t.Fatal(err)}
|
||||
expected:=*channel
|
||||
if err:=db.Model(channel).UpdateColumn("config_json",`{"userTokenSecret":"casesensitive"}`).Error;err!=nil{t.Fatal(err)}
|
||||
updated,err:=repositories.ChannelRepository.UpdatesConfigIfUnchanged(db,&expected,map[string]any{"config_json":`{"requireSignedIdentity":false}`})
|
||||
if err!=nil{t.Fatal(err)}
|
||||
if updated{t.Fatal("case-sensitive secret change was not treated as a conflict")}
|
||||
}
|
||||
|
||||
func TestChannelConfigCompareAndSwapAllowsCurrentEditorAndReset(t *testing.T) {
|
||||
previousDB:=sqls.DB()
|
||||
db:=setupChannelServiceTestDB(t)
|
||||
t.Cleanup(func(){sqls.SetDB(previousDB)})
|
||||
agent:=createChannelServiceTestAgent(t,db,1001)
|
||||
channel,err:=ChannelService.CreateChannel(request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Current policy",Status:int(enums.StatusOk),ConfigJSON:`{"requireSignedIdentity":true}`},channelServiceTestOperator())
|
||||
if err!=nil{t.Fatal(err)}
|
||||
err=ChannelService.UpdateChannel(request.UpdateChannelRequest{ID:channel.ID,CreateChannelRequest:request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Current policy edited",Status:int(enums.StatusOk),ConfigJSON:`{"title":"Updated title"}`}},channelServiceTestOperator())
|
||||
if err!=nil{t.Fatal(err)}
|
||||
secret,err:=ChannelService.ResetUserTokenSecret(channel.ID,channelServiceTestOperator())
|
||||
if err!=nil || secret==""{t.Fatal("current secret reset failed")}
|
||||
cfg,err:=ChannelService.ParseWebChannelConfig(ChannelService.Get(channel.ID).ConfigJSON)
|
||||
if err!=nil || !cfg.RequireSignedIdentity || cfg.UserTokenSecret!=secret{t.Fatal("current update/reset lost policy or returned unsaved secret")}
|
||||
}
|
||||
Reference in New Issue
Block a user