Adds Shared Channel related API endpoints (#27436)

* Adds Shared Channel management API endpoints

New endpoints for the following routes are added:

- Get Shared Channel Remotes by Remote Cluster at `GET
/api/v4/remotecluster/{remote_id}/sharedchannelremotes`
- Invite Remote Cluster to Channel at `POST
/api/v4/remotecluster/{remote_id}/channels/invite`
- Uninvite Remote Cluster to Channel at `POST
/api/v4/remotecluster/{remote_id}/channels/uninvite`

These endpoints are planned to be used from the system console, and
gated through the `manage_secure_connections` permission.

* Adds i18n messages for API errors

* Fix pagination flaky test

* Fix linter

* Adds the posibility of filtering shared channel remotes by home

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Miguel de la Cruz
2024-08-29 12:46:37 +02:00
коммит произвёл GitHub
родитель 60ffd00d30
Коммит 3dc0e63c03
20 изменённых файлов: 819 добавлений и 54 удалений

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

@@ -129,8 +129,10 @@ type Routes struct {
Exports *mux.Router // 'api/v4/exports'
Export *mux.Router // 'api/v4/exports/{export_name:.+\\.zip}'
RemoteCluster *mux.Router // 'api/v4/remotecluster'
SharedChannels *mux.Router // 'api/v4/sharedchannels'
RemoteCluster *mux.Router // 'api/v4/remotecluster'
SharedChannels *mux.Router // 'api/v4/sharedchannels'
ChannelForRemote *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
SharedChannelRemotes *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes'
Permissions *mux.Router // 'api/v4/permissions'
@@ -265,6 +267,8 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.RemoteCluster = api.BaseRoutes.APIRoot.PathPrefix("/remotecluster").Subrouter()
api.BaseRoutes.SharedChannels = api.BaseRoutes.APIRoot.PathPrefix("/sharedchannels").Subrouter()
api.BaseRoutes.SharedChannelRemotes = api.BaseRoutes.RemoteCluster.PathPrefix("/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes").Subrouter()
api.BaseRoutes.ChannelForRemote = api.BaseRoutes.RemoteCluster.PathPrefix("/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter()

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

@@ -8,11 +8,17 @@ import (
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/audit"
)
func (api *API) InitSharedChannels() {
api.BaseRoutes.SharedChannels.Handle("/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getSharedChannels)).Methods(http.MethodGet)
api.BaseRoutes.SharedChannels.Handle("/remote_info/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteClusterInfo)).Methods(http.MethodGet)
api.BaseRoutes.SharedChannelRemotes.Handle("", api.APISessionRequired(getSharedChannelRemotesByRemoteCluster)).Methods(http.MethodGet)
api.BaseRoutes.ChannelForRemote.Handle("/invite", api.APISessionRequired(inviteRemoteClusterToChannel)).Methods(http.MethodPost)
api.BaseRoutes.ChannelForRemote.Handle("/uninvite", api.APISessionRequired(uninviteRemoteClusterToChannel)).Methods(http.MethodPost)
}
func getSharedChannels(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -86,3 +92,146 @@ func getRemoteClusterInfo(c *Context, w http.ResponseWriter, r *http.Request) {
}
w.Write(b)
}
func getSharedChannelRemotesByRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSecureConnections)
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId); appErr != nil {
c.Err = appErr
return
}
filter := model.SharedChannelRemoteFilterOpts{
RemoteId: c.Params.RemoteId,
ExcludeHome: c.Params.ExcludeHome,
ExcludeRemote: c.Params.ExcludeRemote,
}
sharedChannelRemotes, err := c.App.GetSharedChannelRemotes(c.Params.Page, c.Params.PerPage, filter)
if err != nil {
c.Err = model.NewAppError("getSharedChannelRemotesByRemoteCluster", "api.shared_channel.get_shared_channel_remotes_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if err := json.NewEncoder(w).Encode(sharedChannelRemotes); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func inviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSharedChannels)
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId); appErr != nil {
c.SetInvalidRemoteIdError(c.Params.RemoteId)
return
}
if _, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId); appErr != nil {
c.SetInvalidURLParam("channel_id")
return
}
auditRec := c.MakeAuditRecord("inviteRemoteClusterToChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "user_id", c.AppContext.Session().UserId)
if err := c.App.InviteRemoteToChannel(c.Params.ChannelId, c.Params.RemoteId, c.AppContext.Session().UserId, true); err != nil {
c.Err = model.NewAppError("inviteRemoteClusterToChannel", "api.shared_channel.invite_remote_to_channel_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusNoContent)
}
func uninviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
c.SetPermissionError(model.PermissionManageSharedChannels)
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId); appErr != nil {
c.SetInvalidRemoteIdError(c.Params.RemoteId)
return
}
if _, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId); appErr != nil {
c.SetInvalidURLParam("channel_id")
return
}
auditRec := c.MakeAuditRecord("uninviteRemoteClusterToChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
hasRemote, err := c.App.HasRemote(c.Params.ChannelId, c.Params.RemoteId)
if err != nil {
c.Err = model.NewAppError("uninviteRemoteClusterToChannel", "api.shared_channel.has_remote_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
// if the channel is not shared with the remote, we return early
if !hasRemote {
w.WriteHeader(http.StatusNoContent)
return
}
if err := c.App.UninviteRemoteFromChannel(c.Params.ChannelId, c.Params.RemoteId); err != nil {
c.Err = model.NewAppError("uninviteRemoteClusterToChannel", "api.shared_channel.uninvite_remote_to_channel_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusNoContent)
}

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

@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"math/rand"
"net/http"
"sort"
"testing"
"time"
@@ -22,10 +23,16 @@ var (
)
func setupForSharedChannels(tb testing.TB) *TestHelper {
return SetupConfig(tb, func(cfg *model.Config) {
th := SetupConfig(tb, func(cfg *model.Config) {
*cfg.ExperimentalSettings.EnableRemoteClusterService = true
*cfg.ExperimentalSettings.EnableSharedChannels = true
})
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = fmt.Sprintf("http://localhost:%d", th.Server.ListenAddr.Port)
})
return th
}
func TestGetAllSharedChannels(t *testing.T) {
@@ -232,3 +239,278 @@ func TestCreateDirectChannelWithRemoteUser(t *testing.T) {
require.True(t, dm.IsShared())
})
}
func TestGetSharedChannelRemotesByRemoteCluster(t *testing.T) {
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
resp, err := th.SystemAdminClient.DeleteRemoteCluster(context.Background(), model.NewId())
CheckNotImplementedStatus(t, resp)
require.Error(t, err)
})
th := setupForSharedChannels(t).InitBasic()
defer th.TearDown()
newRC1 := &model.RemoteCluster{Name: "rc1", SiteURL: "http://example1.com", CreatorId: th.SystemAdminUser.Id}
newRC2 := &model.RemoteCluster{Name: "rc2", SiteURL: "http://example2.com", CreatorId: th.SystemAdminUser.Id}
rc1, appErr := th.App.AddRemoteCluster(newRC1)
require.Nil(t, appErr)
rc2, appErr := th.App.AddRemoteCluster(newRC2)
require.Nil(t, appErr)
c1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, th.BasicTeam.Id)
sc1 := &model.SharedChannel{
ChannelId: c1.Id,
TeamId: th.BasicTeam.Id,
ShareName: "shared_1",
ShareDisplayName: "Shared Channel 1", // for sorting purposes
CreatorId: th.BasicUser.Id,
RemoteId: rc1.RemoteId,
Home: true,
}
_, err := th.App.ShareChannel(th.Context, sc1)
require.NoError(t, err)
c2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, th.BasicTeam.Id)
sc2 := &model.SharedChannel{
ChannelId: c2.Id,
TeamId: th.BasicTeam.Id,
ShareName: "shared_2",
ShareDisplayName: "Shared Channel 2",
CreatorId: th.BasicUser.Id,
RemoteId: rc1.RemoteId,
Home: false,
}
_, err = th.App.ShareChannel(th.Context, sc2)
require.NoError(t, err)
c3 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, th.BasicTeam.Id)
sc3 := &model.SharedChannel{
ChannelId: c3.Id,
TeamId: th.BasicTeam.Id,
ShareName: "shared_3",
CreatorId: th.BasicUser.Id,
RemoteId: rc2.RemoteId,
}
_, err = th.App.ShareChannel(th.Context, sc3)
require.NoError(t, err)
// for the pagination test, we need to get the channelId of the
// second SharedChannelRemote that belongs to RC1, sorted by ID,
// so we accumulate those SharedChannelRemotes on creation and
// later sort them to be able to get the right one for the test
// result
sharedChannelRemotesFromRC1 := []*model.SharedChannelRemote{}
// create the shared channel remotes
for _, sc := range []*model.SharedChannel{sc1, sc2, sc3} {
scr := &model.SharedChannelRemote{
Id: model.NewId(),
ChannelId: sc.ChannelId,
CreatorId: sc.CreatorId,
IsInviteAccepted: true,
IsInviteConfirmed: true,
RemoteId: sc.RemoteId,
}
_, err = th.App.SaveSharedChannelRemote(scr)
require.NoError(t, err)
if scr.RemoteId == rc1.RemoteId {
sharedChannelRemotesFromRC1 = append(sharedChannelRemotesFromRC1, scr)
}
}
sort.Slice(sharedChannelRemotesFromRC1, func(i, j int) bool {
return sharedChannelRemotesFromRC1[i].Id < sharedChannelRemotesFromRC1[j].Id
})
t.Run("should return the expected shared channels", func(t *testing.T) {
testCases := []struct {
Name string
Client *model.Client4
RemoteId string
ExcludeHome bool
ExcludeRemote bool
Page int
PerPage int
ExpectedStatusCode int
ExpectedError bool
ExpectedIds []string
}{
{
Name: "should not work if the user doesn't have the right permissions",
Client: th.Client,
RemoteId: rc1.RemoteId,
Page: 0,
PerPage: 100,
ExpectedStatusCode: http.StatusForbidden,
ExpectedError: true,
},
{
Name: "should not work if the remote cluster is nonexistent",
Client: th.SystemAdminClient,
RemoteId: model.NewId(),
Page: 0,
PerPage: 100,
ExpectedStatusCode: http.StatusNotFound,
ExpectedError: true,
},
{
Name: "should return the complete list of shared channel remotes for a remote cluster",
Client: th.SystemAdminClient,
RemoteId: rc1.RemoteId,
Page: 0,
PerPage: 100,
ExpectedStatusCode: http.StatusOK,
ExpectedError: false,
ExpectedIds: []string{sc1.ChannelId, sc2.ChannelId},
},
{
Name: "should return only the shared channel remotes homed localy",
Client: th.SystemAdminClient,
RemoteId: rc1.RemoteId,
ExcludeRemote: true,
Page: 0,
PerPage: 100,
ExpectedStatusCode: http.StatusOK,
ExpectedError: false,
ExpectedIds: []string{sc1.ChannelId},
},
{
Name: "should return only the shared channel remotes homed remotely",
Client: th.SystemAdminClient,
RemoteId: rc1.RemoteId,
ExcludeHome: true,
Page: 0,
PerPage: 100,
ExpectedStatusCode: http.StatusOK,
ExpectedError: false,
ExpectedIds: []string{sc2.ChannelId},
},
{
Name: "should correctly paginate the results",
Client: th.SystemAdminClient,
RemoteId: rc1.RemoteId,
Page: 1,
PerPage: 1,
ExpectedStatusCode: http.StatusOK,
ExpectedError: false,
ExpectedIds: []string{sharedChannelRemotesFromRC1[1].ChannelId},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
scrs, resp, err := tc.Client.GetSharedChannelRemotesByRemoteCluster(context.Background(), tc.RemoteId, tc.ExcludeHome, tc.ExcludeRemote, tc.Page, tc.PerPage)
checkHTTPStatus(t, resp, tc.ExpectedStatusCode)
if tc.ExpectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Len(t, scrs, len(tc.ExpectedIds))
foundIds := []string{}
for _, scr := range scrs {
require.Equal(t, tc.RemoteId, scr.RemoteId)
foundIds = append(foundIds, scr.ChannelId)
}
require.ElementsMatch(t, tc.ExpectedIds, foundIds)
})
}
})
}
func TestInviteRemoteClusterToChannel(t *testing.T) {
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
resp, err := th.SystemAdminClient.InviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId())
CheckNotImplementedStatus(t, resp)
require.Error(t, err)
})
th := setupForSharedChannels(t).InitBasic()
defer th.TearDown()
newRC := &model.RemoteCluster{Name: "rc", SiteURL: "http://example.com", CreatorId: th.SystemAdminUser.Id}
rc, appErr := th.App.AddRemoteCluster(newRC)
require.Nil(t, appErr)
t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) {
resp, err := th.Client.InviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId())
CheckForbiddenStatus(t, resp)
require.Error(t, err)
})
t.Run("should not work if the remote cluster is nonexistent", func(t *testing.T) {
resp, err := th.SystemAdminClient.InviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId())
CheckBadRequestStatus(t, resp)
require.Error(t, err)
})
t.Run("should not work if the channel is nonexistent", func(t *testing.T) {
resp, err := th.SystemAdminClient.InviteRemoteClusterToChannel(context.Background(), rc.RemoteId, model.NewId())
CheckBadRequestStatus(t, resp)
require.Error(t, err)
})
t.Run("should correctly invite the remote cluster to the channel", func(t *testing.T) {
t.Skip("Requires server2server communication: ToBeImplemented")
})
t.Run("should do nothing but return 204 if the remote cluster is already invited to the channel", func(t *testing.T) {
t.Skip("Requires server2server communication: ToBeImplemented")
})
}
func TestUninviteRemoteClusterToChannel(t *testing.T) {
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
resp, err := th.SystemAdminClient.UninviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId())
CheckNotImplementedStatus(t, resp)
require.Error(t, err)
})
th := setupForSharedChannels(t).InitBasic()
defer th.TearDown()
newRC := &model.RemoteCluster{Name: "rc", SiteURL: "http://example.com", CreatorId: th.SystemAdminUser.Id}
rc, appErr := th.App.AddRemoteCluster(newRC)
require.Nil(t, appErr)
t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) {
resp, err := th.Client.UninviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId())
CheckForbiddenStatus(t, resp)
require.Error(t, err)
})
t.Run("should not work if the remote cluster is nonexistent", func(t *testing.T) {
resp, err := th.SystemAdminClient.UninviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId())
CheckBadRequestStatus(t, resp)
require.Error(t, err)
})
t.Run("should not work if the channel is nonexistent", func(t *testing.T) {
resp, err := th.SystemAdminClient.UninviteRemoteClusterToChannel(context.Background(), rc.RemoteId, model.NewId())
CheckBadRequestStatus(t, resp)
require.Error(t, err)
})
t.Run("should correctly uninvite the remote cluster to the channel", func(t *testing.T) {
t.Skip("Requires server2server communication: ToBeImplemented")
})
t.Run("should do nothing but return 204 if the remote cluster is not sharing the channel", func(t *testing.T) {
t.Skip("Requires server2server communication: ToBeImplemented")
})
}