diff --git a/api4/channel.go b/api4/channel.go index f7d1396ba4..a41fabc4bf 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -1074,7 +1074,15 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - err = c.App.DeleteChannel(channel, c.App.Session().UserId) + if c.Params.Permanent { + if *c.App.Config().ServiceSettings.EnableAPIChannelDeletion { + err = c.App.PermanentDeleteChannel(channel) + } else { + err = model.NewAppError("deleteChannel", "api.user.delete_channel.not_enabled.app_error", nil, "channelId="+c.Params.ChannelId, http.StatusUnauthorized) + } + } else { + err = c.App.DeleteChannel(channel, c.App.Session().UserId) + } if err != nil { c.Err = err return diff --git a/api4/channel_local.go b/api4/channel_local.go index 56fc7a9308..c1a3f30dae 100644 --- a/api4/channel_local.go +++ b/api4/channel_local.go @@ -15,7 +15,7 @@ func (api *API) InitChannelLocal() { api.BaseRoutes.Channels.Handle("", api.ApiLocal(localCreateChannel)).Methods("POST") api.BaseRoutes.Channel.Handle("", api.ApiLocal(getChannel)).Methods("GET") api.BaseRoutes.ChannelByName.Handle("", api.ApiLocal(getChannelByName)).Methods("GET") - api.BaseRoutes.Channel.Handle("", api.ApiLocal(deleteChannel)).Methods("DELETE") + api.BaseRoutes.Channel.Handle("", api.ApiLocal(localDeleteChannel)).Methods("DELETE") api.BaseRoutes.Channel.Handle("/patch", api.ApiLocal(localPatchChannel)).Methods("PUT") api.BaseRoutes.Channel.Handle("/move", api.ApiLocal(localMoveChannel)).Methods("POST") @@ -277,3 +277,40 @@ func localMoveChannel(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(channel.ToJson())) } + +func localDeleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId() + if c.Err != nil { + return + } + + channel, err := c.App.GetChannel(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + auditRec := c.MakeAuditRecord("localDeleteChannel", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("channeld", channel) + + if channel.Type == model.CHANNEL_DIRECT || channel.Type == model.CHANNEL_GROUP { + c.Err = model.NewAppError("localDeleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest) + return + } + + if c.Params.Permanent { + err = c.App.PermanentDeleteChannel(channel) + } else { + err = c.App.DeleteChannel(channel, "") + } + if err != nil { + c.Err = err + return + } + + auditRec.Success() + c.LogAudit("name=" + channel.Name) + + ReturnStatusOK(w) +} diff --git a/api4/channel_test.go b/api4/channel_test.go index c776fb1418..e068932446 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1629,6 +1629,45 @@ func TestDeleteChannel2(t *testing.T) { CheckForbiddenStatus(t, resp) } +func TestPermanentDeleteChannel(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + enableAPIChannelDeletion := *th.App.Config().ServiceSettings.EnableAPIChannelDeletion + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableAPIChannelDeletion = &enableAPIChannelDeletion }) + }() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = false }) + + publicChannel1 := th.CreatePublicChannel() + t.Run("Permanent deletion not available through API if EnableAPIChannelDeletion is not set", func(t *testing.T) { + _, resp := th.SystemAdminClient.PermanentDeleteChannel(publicChannel1.Id) + CheckUnauthorizedStatus(t, resp) + }) + + t.Run("Permanent deletion available through local mode even if EnableAPIChannelDeletion is not set", func(t *testing.T) { + ok, resp := th.LocalClient.PermanentDeleteChannel(publicChannel1.Id) + CheckNoError(t, resp) + assert.True(t, ok) + }) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = true }) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { + publicChannel := th.CreatePublicChannel() + ok, resp := c.PermanentDeleteChannel(publicChannel.Id) + CheckNoError(t, resp) + assert.True(t, ok) + + _, err := th.App.GetChannel(publicChannel.Id) + assert.NotNil(t, err) + + ok, resp = c.PermanentDeleteChannel("junk") + CheckBadRequestStatus(t, resp) + require.False(t, ok, "should have returned false") + }, "Permanent deletion with EnableAPIChannelDeletion set") +} + func TestConvertChannelToPrivate(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/channel.go b/app/channel.go index 27fc1588fe..3810650376 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2345,10 +2345,18 @@ func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError { return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_outgoing_by_channel.app_error", nil, err.Error(), http.StatusInternalServerError) } + deleteAt := model.GetMillis() + if nErr := a.Srv().Store.Channel().PermanentDelete(channel.Id); nErr != nil { return model.NewAppError("PermanentDeleteChannel", "app.channel.permanent_delete.app_error", nil, nErr.Error(), http.StatusInternalServerError) } + a.invalidateCacheForChannel(channel) + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, channel.TeamId, "", "", nil) + message.Add("channel_id", channel.Id) + message.Add("delete_at", deleteAt) + a.Publish(message) + return nil } diff --git a/i18n/en.json b/i18n/en.json index f9c590c0f2..051e44b2b3 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2754,6 +2754,10 @@ "id": "api.user.create_user.signup_link_invalid.app_error", "translation": "The signup link does not appear to be valid." }, + { + "id": "api.user.delete_channel.not_enabled.app_error", + "translation": "Permanent channel deletion feature is not enabled. Please contact your System Administrator." + }, { "id": "api.user.delete_team.not_enabled.app_error", "translation": "Permanent team deletion feature is not enabled. Please contact your System Administrator." diff --git a/model/client4.go b/model/client4.go index 2557460b2f..715c862358 100644 --- a/model/client4.go +++ b/model/client4.go @@ -2562,6 +2562,16 @@ func (c *Client4) DeleteChannel(channelId string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } +// PermanentDeleteChannel deletes a channel based on the provided channel id string. +func (c *Client4) PermanentDeleteChannel(channelId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetChannelRoute(channelId) + "?permanent=" + c.boolString(true)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // MoveChannel moves the channel to the destination team. func (c *Client4) MoveChannel(channelId, teamId string, force bool) (*Channel, *Response) { requestBody := map[string]interface{}{ diff --git a/model/config.go b/model/config.go index e7c9d1ffeb..da40ec1d38 100644 --- a/model/config.go +++ b/model/config.go @@ -329,6 +329,7 @@ type ServiceSettings struct { DEPRECATED_DO_NOT_USE_ImageProxyOptions *string `json:"ImageProxyOptions" mapstructure:"ImageProxyOptions"` // This field is deprecated and must not be used. EnableAPITeamDeletion *bool EnableAPIUserDeletion *bool + EnableAPIChannelDeletion *bool ExperimentalEnableHardenedMode *bool DisableLegacyMFA *bool `restricted:"true"` ExperimentalStrictCSRFEnforcement *bool `restricted:"true"` @@ -705,6 +706,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { s.EnableAPIUserDeletion = NewBool(false) } + if s.EnableAPIChannelDeletion == nil { + s.EnableAPIChannelDeletion = NewBool(false) + } + if s.ExperimentalEnableHardenedMode == nil { s.ExperimentalEnableHardenedMode = NewBool(false) }