fix(admin): close PR review blockers
Keep bot credentials out of durable command results, fail bot deletion closed when session revocation fails, reject invalid scam/fake states at every write boundary, and make direct collectible grants a single replayable PostgreSQL aggregate. Also lock admin gift sender/message limits and add regression coverage for rollback, replay, moderation constraints, and credential redaction.
This commit is contained in:
parent
90792cdfab
commit
234061ef83
30 changed files with 859 additions and 93 deletions
|
|
@ -299,6 +299,10 @@ type CommandResult struct {
|
|||
Message string `json:"message"`
|
||||
Details map[string]any `json:"details,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
// transientDetails are returned to the initiating caller only. They are
|
||||
// deliberately excluded from JSON so credentials can never enter command
|
||||
// replay or audit storage.
|
||||
transientDetails map[string]any
|
||||
}
|
||||
|
||||
type ImportStarGiftRequest struct {
|
||||
|
|
@ -343,14 +347,14 @@ type SetStarGiftSortOrderRequest struct {
|
|||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// GiveGiftRequest grants a catalog gift to a recipient (user or channel) from a
|
||||
// sender account (defaults to the official system account 777000) at no charge.
|
||||
// GiveGiftRequest grants a catalog gift to a recipient (user or channel) from
|
||||
// the official system account 777000 at no charge.
|
||||
// Exactly one of UserID / ChannelID identifies the recipient.
|
||||
type GiveGiftRequest struct {
|
||||
CommandMeta
|
||||
SenderUserID int64 `json:"sender_user_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
SenderUserID int64 `json:"sender_user_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
GiftID int64 `json:"gift_id"`
|
||||
HideName bool `json:"hide_name"`
|
||||
Message string `json:"message"`
|
||||
|
|
@ -870,10 +874,8 @@ func (s *Service) SetUserFlags(ctx context.Context, req SetUserFlagsRequest) (Co
|
|||
if s == nil || s.users == nil {
|
||||
return CommandResult{}, fmt.Errorf("admin user dependency is not configured")
|
||||
}
|
||||
// scam and fake are mutually exclusive (a peer is never both in Telegram).
|
||||
// scam takes precedence so the two never persist together.
|
||||
if req.Scam {
|
||||
req.Fake = false
|
||||
if req.Scam && req.Fake {
|
||||
return CommandResult{}, domain.ErrPeerModerationFlagsInvalid
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionSetUserFlags, req.UserID, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
u, found, err := s.users.AdminUser(ctx, req.UserID)
|
||||
|
|
@ -945,9 +947,9 @@ func collectibleAttrPresent(attrs []domain.StarGiftCollectibleAttribute, id int6
|
|||
return false
|
||||
}
|
||||
|
||||
// GiveGift grants a catalog gift to a recipient (user or channel) from a sender
|
||||
// account (defaults to the official system account 777000) without charging any
|
||||
// Stars. Delivery reuses the standard gift path via the GiftGranter dependency.
|
||||
// GiveGift grants a catalog gift to a recipient (user or channel) from the
|
||||
// official system account 777000 without charging any Stars. Delivery reuses
|
||||
// the standard gift path via the GiftGranter dependency.
|
||||
func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandResult, error) {
|
||||
if req.GiftID <= 0 {
|
||||
return CommandResult{}, fmt.Errorf("gift_id is required")
|
||||
|
|
@ -962,6 +964,13 @@ func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandRes
|
|||
if sender <= 0 {
|
||||
sender = domain.OfficialSystemUserID
|
||||
}
|
||||
if sender != domain.OfficialSystemUserID {
|
||||
return CommandResult{}, fmt.Errorf("gift sender must be the official system account")
|
||||
}
|
||||
req.Message = strings.TrimSpace(req.Message)
|
||||
if len([]rune(req.Message)) > 128 {
|
||||
return CommandResult{}, fmt.Errorf("gift message must be <= 128 characters")
|
||||
}
|
||||
var recipient domain.Peer
|
||||
if req.ChannelID > 0 {
|
||||
recipient = domain.Peer{Type: domain.PeerTypeChannel, ID: req.ChannelID}
|
||||
|
|
@ -983,8 +992,8 @@ func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandRes
|
|||
"hide_name": req.HideName,
|
||||
"upgrade": req.Upgrade,
|
||||
}
|
||||
if strings.TrimSpace(req.Message) != "" {
|
||||
details["message"] = strings.TrimSpace(req.Message)
|
||||
if req.Message != "" {
|
||||
details["message"] = req.Message
|
||||
}
|
||||
if s.gifts != nil {
|
||||
gift, found, err := s.gifts.GiftByID(ctx, req.GiftID)
|
||||
|
|
@ -1037,8 +1046,9 @@ func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandRes
|
|||
Recipient: recipient,
|
||||
GiftID: req.GiftID,
|
||||
HideName: req.HideName,
|
||||
Message: strings.TrimSpace(req.Message),
|
||||
Message: req.Message,
|
||||
Upgrade: req.Upgrade,
|
||||
CommandKey: "admin-gift:" + req.CommandID,
|
||||
ModelAttributeID: req.ModelAttributeID,
|
||||
PatternAttributeID: req.PatternAttributeID,
|
||||
BackdropAttributeID: req.BackdropAttributeID,
|
||||
|
|
@ -1173,14 +1183,14 @@ func (s *Service) CreateBot(ctx context.Context, req CreateBotRequest) (CommandR
|
|||
return CommandResult{Details: details}, err
|
||||
}
|
||||
details["bot_user_id"] = bot.ID
|
||||
// The token is a credential. It is surfaced once so the operator can copy
|
||||
// it; it is also persisted in the audit result, so treat admin audit logs
|
||||
// as sensitive.
|
||||
details["token"] = token
|
||||
if err := s.notifyUserChanged(ctx, bot); err != nil {
|
||||
details["notify_error"] = err.Error()
|
||||
}
|
||||
return CommandResult{Message: "bot created", Details: details}, nil
|
||||
return CommandResult{
|
||||
Message: "bot created",
|
||||
Details: details,
|
||||
transientDetails: map[string]any{"token": token},
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1274,6 +1284,9 @@ func (s *Service) SetChannelFlags(ctx context.Context, req SetChannelFlagsReques
|
|||
if s == nil || s.channels == nil {
|
||||
return CommandResult{}, fmt.Errorf("admin channel dependency is not configured")
|
||||
}
|
||||
if req.Scam && req.Fake {
|
||||
return CommandResult{}, domain.ErrPeerModerationFlagsInvalid
|
||||
}
|
||||
target := domain.Peer{Type: domain.PeerTypeChannel, ID: req.ChannelID}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionSetChannelFlags, 0, target, req, func() (CommandResult, error) {
|
||||
ch, err := s.channels.GetChannelByID(ctx, req.ChannelID)
|
||||
|
|
@ -2092,14 +2105,24 @@ func (s *Service) runCommand(ctx context.Context, meta CommandMeta, action strin
|
|||
if marshalErr != nil {
|
||||
return result, fmt.Errorf("marshal admin result: %w", marshalErr)
|
||||
}
|
||||
response := result
|
||||
if len(result.transientDetails) > 0 {
|
||||
response.Details = make(map[string]any, len(result.Details)+len(result.transientDetails))
|
||||
for key, value := range result.Details {
|
||||
response.Details[key] = value
|
||||
}
|
||||
for key, value := range result.transientDetails {
|
||||
response.Details[key] = value
|
||||
}
|
||||
}
|
||||
errorText := ""
|
||||
if opErr != nil {
|
||||
errorText = opErr.Error()
|
||||
}
|
||||
if _, err := s.commands.FinishCommand(ctx, meta.CommandID, status, resultJSON, errorText); err != nil {
|
||||
return result, err
|
||||
return response, err
|
||||
}
|
||||
return result, opErr
|
||||
return response, opErr
|
||||
}
|
||||
|
||||
func sameJSON(a, b []byte) bool {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue