[MM-62427] Add message attachments validation (#30180)

* Add message attachments validation

* Add props validation

* Validate slack attachment fields

* Update tests and library usage

* Improve interactive dialog error for length checks

* Allow predefined colors for slack attachments

* Fix TestPostAction

* Use const for data source

* Add tests

* Cleanup unused props

* Add happy path tests

* lint fixes

* Add validation for PostActionOptions
Этот коммит содержится в:
Ben Schumacher
2025-03-20 12:53:50 +01:00
коммит произвёл GitHub
родитель 5609489e86
Коммит 9b5d8d52bf
47 изменённых файлов: 1402 добавлений и 355 удалений

Просмотреть файл

@@ -15,7 +15,9 @@ import (
"sync"
"unicode/utf8"
"github.com/hashicorp/go-multierror"
"github.com/mattermost/mattermost/server/public/shared/markdown"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
const (
@@ -72,14 +74,16 @@ const (
PostPropsFromBot = "from_bot"
PostPropsFromOAuthApp = "from_oauth_app"
PostPropsWebhookDisplayName = "webhook_display_name"
PostPropsAttachments = "attachments"
PostPropsFromPlugin = "from_plugin"
PostPropsMentionHighlightDisabled = "mentionHighlightDisabled"
PostPropsGroupHighlightDisabled = "disable_group_highlight"
PostPropsPreviewedPost = "previewed_post"
PostPropsForceNotification = "force_notification"
PostPropsChannelMentions = "channel_mentions"
PostPropsUnsafeLinks = "unsafe_links"
PostPriorityUrgent = "urgent"
PostPropsRequestedAck = "requested_ack"
PostPropsPersistentNotifications = "persistent_notifications"
PostPriorityUrgent = "urgent"
)
type Post struct {
@@ -657,6 +661,137 @@ func (o *Post) GetProp(key string) any {
return o.Props[key]
}
// ValidateProps checks all known props for validity.
// Currently, it logs warnings for invalid props rather than returning an error.
// In a future version, this will be updated to return errors for invalid props.
func (o *Post) ValidateProps(logger mlog.LoggerIFace) {
if err := o.propsIsValid(); err != nil {
logger.Warn(
"Invalid post props. In a future version this will result in an error. Please update your integration to be compliant.",
mlog.String("post_id", o.Id),
mlog.Err(err),
)
}
}
func (o *Post) propsIsValid() error {
var multiErr *multierror.Error
props := o.GetProps()
// Check basic props validity
if props == nil {
return nil
}
if props[PostPropsAddedUserId] != nil {
if addedUserID, ok := props[PostPropsAddedUserId].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("added_user_id prop must be a string"))
} else if !IsValidId(addedUserID) {
multiErr = multierror.Append(multiErr, fmt.Errorf("added_user_id prop must be a valid user ID"))
}
}
if props[PostPropsDeleteBy] != nil {
if deleteByID, ok := props[PostPropsDeleteBy].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("delete_by prop must be a string"))
} else if !IsValidId(deleteByID) {
multiErr = multierror.Append(multiErr, fmt.Errorf("delete_by prop must be a valid user ID"))
}
}
// Validate integration props
if props[PostPropsOverrideIconURL] != nil {
if iconURL, ok := props[PostPropsOverrideIconURL].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("override_icon_url prop must be a string"))
} else if iconURL == "" || !IsValidHTTPURL(iconURL) {
multiErr = multierror.Append(multiErr, fmt.Errorf("override_icon_url prop must be a valid URL"))
}
}
if props[PostPropsOverrideIconEmoji] != nil {
if _, ok := props[PostPropsOverrideIconEmoji].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("override_icon_emoji prop must be a string"))
}
}
if props[PostPropsOverrideUsername] != nil {
if _, ok := props[PostPropsOverrideUsername].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("override_username prop must be a string"))
}
}
if props[PostPropsFromWebhook] != nil {
if fromWebhook, ok := props[PostPropsFromWebhook].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_webhook prop must be a string"))
} else if fromWebhook != "true" {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_webhook prop must be \"true\""))
}
}
if props[PostPropsFromBot] != nil {
if fromBot, ok := props[PostPropsFromBot].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_bot prop must be a string"))
} else if fromBot != "true" {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_bot prop must be \"true\""))
}
}
if props[PostPropsFromOAuthApp] != nil {
if fromOAuthApp, ok := props[PostPropsFromOAuthApp].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_oauth_app prop must be a string"))
} else if fromOAuthApp != "true" {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_oauth_app prop must be \"true\""))
}
}
if props[PostPropsFromPlugin] != nil {
if fromPlugin, ok := props[PostPropsFromPlugin].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_plugin prop must be a string"))
} else if fromPlugin != "true" {
multiErr = multierror.Append(multiErr, fmt.Errorf("from_plugin prop must be \"true\""))
}
}
if props[PostPropsUnsafeLinks] != nil {
if unsafeLinks, ok := props[PostPropsUnsafeLinks].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("unsafe_links prop must be a string"))
} else if unsafeLinks != "true" {
multiErr = multierror.Append(multiErr, fmt.Errorf("unsafe_links prop must be \"true\""))
}
}
if props[PostPropsWebhookDisplayName] != nil {
if _, ok := props[PostPropsWebhookDisplayName].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("webhook_display_name prop must be a string"))
}
}
if props[PostPropsMentionHighlightDisabled] != nil {
if _, ok := props[PostPropsMentionHighlightDisabled].(bool); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("mention_highlight_disabled prop must be a boolean"))
}
}
if props[PostPropsGroupHighlightDisabled] != nil {
if _, ok := props[PostPropsGroupHighlightDisabled].(bool); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("disable_group_highlight prop must be a boolean"))
}
}
if props[PostPropsPreviewedPost] != nil {
if previewedPostID, ok := props[PostPropsPreviewedPost].(string); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("previewed_post prop must be a string"))
} else if !IsValidId(previewedPostID) {
multiErr = multierror.Append(multiErr, fmt.Errorf("previewed_post prop must be a valid post ID"))
}
}
if props[PostPropsForceNotification] != nil {
if _, ok := props[PostPropsForceNotification].(bool); !ok {
multiErr = multierror.Append(multiErr, fmt.Errorf("force_notification prop must be a boolean"))
}
}
for i, a := range o.Attachments() {
if err := a.IsValid(); err != nil {
multiErr = multierror.Append(multiErr, multierror.Prefix(err, fmt.Sprintf("message attachtment at index %d is invalid:", i)))
}
}
return multiErr.ErrorOrNil()
}
func (o *Post) IsSystemMessage() bool {
return len(o.Type) >= len(PostSystemMessagePrefix) && o.Type[:len(PostSystemMessagePrefix)] == PostSystemMessagePrefix
}
@@ -746,11 +881,11 @@ func findAtChannelMention(message string) (mention string, found bool) {
}
func (o *Post) Attachments() []*SlackAttachment {
if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
if attachments, ok := o.GetProp(PostPropsAttachments).([]*SlackAttachment); ok {
return attachments
}
var ret []*SlackAttachment
if attachments, ok := o.GetProp("attachments").([]any); ok {
if attachments, ok := o.GetProp(PostPropsAttachments).([]any); ok {
for _, attachment := range attachments {
if enc, err := json.Marshal(attachment); err == nil {
var decoded SlackAttachment
@@ -885,7 +1020,7 @@ func RewriteImageURLs(message string, f func(string) string) string {
func (o *Post) IsFromOAuthBot() bool {
props := o.GetProps()
return props["from_webhook"] == "true" && props["override_username"] != ""
return props[PostPropsFromWebhook] == "true" && props[PostPropsOverrideUsername] != ""
}
func (o *Post) ToNilIfInvalid() *Post {