feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Modified in the independent hardening fork; see docs/hardening.
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
@@ -98,6 +100,15 @@ 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 +129,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 +207,13 @@ 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 +477,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 +701,54 @@ 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user