From dad9cab48327b9638c37dd76a213984a23c19d68 Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Fri, 27 Mar 2026 16:51:29 +0100 Subject: [PATCH] Add guards to avoid cards being created when the integrated boards feature flag is disabled (#35836) --- server/channels/api4/post.go | 5 ++ server/channels/api4/post_test.go | 67 +++++++++++++++---- server/channels/api4/post_utils.go | 7 ++ server/channels/api4/scheduled_post.go | 5 ++ server/channels/app/post_permission_utils.go | 9 +++ .../app/post_permission_utils_test.go | 43 ++++++++++++ server/channels/app/scheduled_post_job.go | 12 ++++ server/i18n/en.json | 4 ++ 8 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 server/channels/app/post_permission_utils_test.go diff --git a/server/channels/api4/post.go b/server/channels/api4/post.go index 785b403abb7..a6fcf0e2bcb 100644 --- a/server/channels/api4/post.go +++ b/server/channels/api4/post.go @@ -85,6 +85,11 @@ func createPostChecks(where string, c *Context, post *model.Post) { return } + postCardTypeCheckWithContext(where, c, post.Type) + if c.Err != nil { + return + } + postBurnOnReadCheckWithContext(where, c, post, nil) } diff --git a/server/channels/api4/post_test.go b/server/channels/api4/post_test.go index 1ecc1b8fbd7..5d75449003b 100644 --- a/server/channels/api4/post_test.go +++ b/server/channels/api4/post_test.go @@ -222,19 +222,6 @@ func TestCreatePost(t *testing.T) { assert.Nil(t, rpost) }) - t.Run("with type card", func(t *testing.T) { - cardPost, resp, err := client.CreatePost(context.Background(), &model.Post{ - ChannelId: th.BasicChannel.Id, - Message: "card post", - Type: model.PostTypeCard, - }) - require.NoError(t, err) - CheckCreatedStatus(t, resp) - require.NotNil(t, cardPost) - assert.Equal(t, model.PostTypeCard, cardPost.Type) - assert.Equal(t, "card post", cardPost.Message) - }) - t.Run("invalid post type", func(t *testing.T) { post := basicPost() post.Type = model.PostTypeSystemGeneric @@ -6751,3 +6738,57 @@ func TestPatchCardPostByNonOwner(t *testing.T) { CheckForbiddenStatus(t, resp) }) } + +func TestCreateCardPostWithFeatureFlagDisabled(t *testing.T) { + mainHelper.Parallel(t) + + t.Run("card post rejected when IntegratedBoards flag is disabled", func(t *testing.T) { + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = false + }).InitBasic(t) + client := th.Client + + post := &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "this is a card", + Type: model.PostTypeCard, + } + _, resp, err := client.CreatePost(context.Background(), post) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + + t.Run("card post allowed when IntegratedBoards flag is enabled", func(t *testing.T) { + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = true + }).InitBasic(t) + client := th.Client + + post := &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "card post", + Type: model.PostTypeCard, + } + rpost, resp, err := client.CreatePost(context.Background(), post) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + require.NotNil(t, rpost) + assert.Equal(t, model.PostTypeCard, rpost.Type) + assert.Equal(t, "card post", rpost.Message) + }) + + t.Run("non-card post allowed when IntegratedBoards flag is disabled", func(t *testing.T) { + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = false + }).InitBasic(t) + client := th.Client + + post := &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "this is a regular post", + } + rpost, _, err := client.CreatePost(context.Background(), post) + require.NoError(t, err) + assert.Equal(t, "", rpost.Type) + }) +} diff --git a/server/channels/api4/post_utils.go b/server/channels/api4/post_utils.go index 9fc40da80be..37ceced1504 100644 --- a/server/channels/api4/post_utils.go +++ b/server/channels/api4/post_utils.go @@ -42,6 +42,13 @@ func postPriorityCheckWithContext(where string, c *Context, priority *model.Post } } +func postCardTypeCheckWithContext(where string, c *Context, postType string) { + if appErr := app.PostCardTypeCheckWithApp(where, c.App, postType); appErr != nil { + appErr.Where = where + c.Err = appErr + } +} + func postBurnOnReadCheckWithContext(where string, c *Context, post *model.Post, channel *model.Channel) { appErr := app.PostBurnOnReadCheckWithApp(where, c.App, c.AppContext, post.UserId, post.ChannelId, post.Type, channel) if appErr != nil { diff --git a/server/channels/api4/scheduled_post.go b/server/channels/api4/scheduled_post.go index dcbf27a5209..9a5693be2ad 100644 --- a/server/channels/api4/scheduled_post.go +++ b/server/channels/api4/scheduled_post.go @@ -44,6 +44,11 @@ func scheduledPostChecks(where string, c *Context, scheduledPost *model.Schedule return } + postCardTypeCheckWithContext(where, c, scheduledPost.Type) + if c.Err != nil { + return + } + // Validate burn-on-read restrictions for scheduled post post := &model.Post{ ChannelId: scheduledPost.ChannelId, diff --git a/server/channels/app/post_permission_utils.go b/server/channels/app/post_permission_utils.go index cdd98b95772..d6768e114ee 100644 --- a/server/channels/app/post_permission_utils.go +++ b/server/channels/app/post_permission_utils.go @@ -115,6 +115,15 @@ func userCreatePostPermissionCheckWithApp(rctx request.CTX, a *App, userId, chan return nil } +// PostCardTypeCheckWithApp validates whether a card post can be created +// based on the IntegratedBoards feature flag. +func PostCardTypeCheckWithApp(where string, a *App, postType string) *model.AppError { + if postType == model.PostTypeCard && !a.Config().FeatureFlags.IntegratedBoards { + return model.NewAppError(where, "api.post.create_post.card_type_disabled.app_error", nil, "", http.StatusBadRequest) + } + return nil +} + // PostBurnOnReadCheckWithApp validates whether a burn-on-read post can be created // based on channel type and participants. This is called from the API layer before // post creation to enforce burn-on-read restrictions. diff --git a/server/channels/app/post_permission_utils_test.go b/server/channels/app/post_permission_utils_test.go new file mode 100644 index 00000000000..a70e0296deb --- /dev/null +++ b/server/channels/app/post_permission_utils_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/stretchr/testify/assert" +) + +func TestPostCardTypeCheckWithApp(t *testing.T) { + mainHelper.Parallel(t) + + t.Run("returns error for card post when IntegratedBoards is disabled", func(t *testing.T) { + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = false + }) + + appErr := PostCardTypeCheckWithApp("test", th.App, model.PostTypeCard) + assert.NotNil(t, appErr) + assert.Equal(t, "api.post.create_post.card_type_disabled.app_error", appErr.Id) + }) + + t.Run("returns nil for card post when IntegratedBoards is enabled", func(t *testing.T) { + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = true + }) + + appErr := PostCardTypeCheckWithApp("test", th.App, model.PostTypeCard) + assert.Nil(t, appErr) + }) + + t.Run("returns nil for non-card post when IntegratedBoards is disabled", func(t *testing.T) { + th := SetupConfig(t, func(cfg *model.Config) { + cfg.FeatureFlags.IntegratedBoards = false + }) + + appErr := PostCardTypeCheckWithApp("test", th.App, "") + assert.Nil(t, appErr) + }) +} diff --git a/server/channels/app/scheduled_post_job.go b/server/channels/app/scheduled_post_job.go index 36d3cafb93c..8e64e1f2b28 100644 --- a/server/channels/app/scheduled_post_job.go +++ b/server/channels/app/scheduled_post_job.go @@ -314,6 +314,18 @@ func (a *App) canPostScheduledPost(rctx request.CTX, scheduledPost *model.Schedu return model.ScheduledPostErrorInvalidPost, nil } + if appErr := PostCardTypeCheckWithApp("ScheduledPostJob.postChecks", a, scheduledPost.Type); appErr != nil { + rctx.Logger().Debug( + "canPostScheduledPost card type disabled", + mlog.String("scheduled_post_id", scheduledPost.Id), + mlog.String("user_id", scheduledPost.UserId), + mlog.String("channel_id", scheduledPost.ChannelId), + mlog.String("error_code", model.ScheduledPostErrorInvalidPost), + mlog.Err(appErr), + ) + return model.ScheduledPostErrorInvalidPost, nil + } + // Validate burn-on-read restrictions for scheduled post if appErr := PostBurnOnReadCheckWithApp("ScheduledPostJob.postChecks", a, rctx, scheduledPost.UserId, scheduledPost.ChannelId, scheduledPost.Type, channel); appErr != nil { rctx.Logger().Debug( diff --git a/server/i18n/en.json b/server/i18n/en.json index 33f42dbc3a1..9d2c60882dd 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2780,6 +2780,10 @@ "id": "api.post.create_post.can_not_post_to_deleted.error", "translation": "Can not post to deleted channel." }, + { + "id": "api.post.create_post.card_type_disabled.app_error", + "translation": "Card posts are not enabled on this server." + }, { "id": "api.post.create_post.channel_root_id.app_error", "translation": "Invalid ChannelId for RootId parameter."