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:
2026-10-16 12:15:20 +00:00
parent e57610a27c
commit 63e0457886
86 changed files with 5174 additions and 1929 deletions

View File

@@ -1,3 +1,5 @@
// Modified in the independent hardening fork; see docs/hardening.
package services
import (
@@ -98,6 +100,13 @@ func (s *channelService) UpdateChannel(req request.UpdateChannelRequest, operato
if current == nil || current.Status == enums.StatusDeleted {
return errorsx.InvalidParamI18n("error.e0208")
}
if current.ChannelType == enums.ChannelTypeWeb && strings.TrimSpace(req.ChannelType) == enums.ChannelTypeWeb {
// Older dashboard clients do not expose this policy yet. Omitting the field
// during an unrelated edit must not silently turn authenticated-only off.
merged, err := s.preserveWebIdentityPolicy(req.ConfigJSON, current.ConfigJSON)
if err != nil { return err }
req.ConfigJSON = merged
}
item, err := s.buildChannelModel(req.ID, req.CreateChannelRequest)
if err != nil {
return err
@@ -118,7 +127,7 @@ func (s *channelService) UpdateChannel(req request.UpdateChannelRequest, operato
if item.AIAgentRolloutPercent != current.AIAgentRolloutPercent {
columns["previous_ai_agent_rollout_percent"] = current.AIAgentRolloutPercent
}
return repositories.ChannelRepository.Updates(sqls.DB(), req.ID, columns)
return s.updateConfigIfUnchanged(current, columns)
}
// RollbackChannelAIAgentRollout restores the last channel-level rollout value
@@ -196,6 +205,11 @@ func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfi
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
return nil, err
}
var fields map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &fields); err != nil { return nil, err }
if policy, ok := fields["requireSignedIdentity"]; ok && string(policy) != "true" && string(policy) != "false" {
return nil, errorsx.InvalidParamI18n("error.e0060")
}
}
cfg.Title = strings.TrimSpace(cfg.Title)
if cfg.Title == "" {
@@ -459,7 +473,7 @@ func (s *channelService) ResetUserTokenSecret(channelID int64, operator *dto.Aut
}
configJSON = string(raw)
}
if err := repositories.ChannelRepository.Updates(sqls.DB(), channelID, map[string]any{
if err := s.updateConfigIfUnchanged(channel, map[string]any{
"config_json": configJSON,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
@@ -683,3 +697,36 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe
Remark: strings.TrimSpace(req.Remark),
}, nil
}
// RequiresSignedIdentity enforces the channel policy on the server, not only in
// the embedding SDK. Invalid web configuration fails closed.
func (s *channelService) RequiresSignedIdentity(channel *models.Channel) bool {
if channel == nil { return true }
if channel.ChannelType != enums.ChannelTypeWeb { return false }
cfg, err := s.ParseWebChannelConfig(channel.ConfigJSON)
return err != nil || cfg.RequireSignedIdentity
}
func (s *channelService) preserveWebIdentityPolicy(raw, previousRaw string) (string, error) {
fields := map[string]json.RawMessage{}
if strings.TrimSpace(raw) != "" {
if err := json.Unmarshal([]byte(raw), &fields); err != nil { return "", errorsx.InvalidParamI18n("error.e0060") }
}
if _, present := fields["requireSignedIdentity"]; present { return raw, nil }
previous, err := s.ParseWebChannelConfig(previousRaw)
if err != nil { return "", errorsx.InvalidParamI18n("error.e0060") }
if !previous.RequireSignedIdentity { return raw, nil }
if fields == nil { fields = map[string]json.RawMessage{} }
fields["requireSignedIdentity"] = json.RawMessage("true")
encoded, err := json.Marshal(fields)
return string(encoded), err
}
// Reject rather than retry a stale complete config document: retrying could turn
// an old explicit field into authority to overwrite the new policy/secret.
func (s *channelService) updateConfigIfUnchanged(expected *models.Channel, columns map[string]any) error {
updated, err := repositories.ChannelRepository.UpdatesConfigIfUnchanged(sqls.DB(), expected, columns)
if err != nil { return errorsx.BusinessErrorI18n(0, "error.channel.configUpdateFailed") }
if !updated { return errorsx.InvalidParamI18n("error.channel.configChanged") }
return nil
}