Adds Remote Cluster related API endpoints (#27432)
* Adds Remote Cluster related API endpoints
New endpoints for the following routes are added:
- Get Remote Clusters at `GET /api/v4/remotecluster`
- Create Remote Cluster at `POST /api/v4/remotecluster`
- Accept Remote Cluster invite at `POST
/api/v4/remotecluster/accept_invite`
- Generate Remote Cluster invite at `POST
/api/v4/remotecluster/{remote_id}/generate_invite`
- Get Remote Cluster at `GET /api/v4/remotecluster/{remote_id}`
- Patch Remote Cluster at `PATCH /api/v4/remotecluster/{remote_id}`
- Delete Remote Cluster at `DELETE /api/v4/remotecluster/{remote_id}`
These endpoints are planned to be used from the system console, and
gated through the `manage_secure_connections` permission.
* Update server/channels/api4/remote_cluster_test.go
Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
* Fix AppError names
---------
Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
cc5e87ae24
Коммит
809ad4f76d
@@ -1086,6 +1086,11 @@ func CheckCreatedStatus(tb testing.TB, resp *model.Response) {
|
||||
checkHTTPStatus(tb, resp, http.StatusCreated)
|
||||
}
|
||||
|
||||
func CheckNoContentStatus(tb testing.TB, resp *model.Response) {
|
||||
tb.Helper()
|
||||
checkHTTPStatus(tb, resp, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func CheckForbiddenStatus(tb testing.TB, resp *model.Response) {
|
||||
tb.Helper()
|
||||
checkHTTPStatus(tb, resp, http.StatusForbidden)
|
||||
@@ -1106,6 +1111,11 @@ func CheckBadRequestStatus(tb testing.TB, resp *model.Response) {
|
||||
checkHTTPStatus(tb, resp, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func CheckUnprocessableEntityStatus(tb testing.TB, resp *model.Response) {
|
||||
tb.Helper()
|
||||
checkHTTPStatus(tb, resp, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func CheckNotImplementedStatus(tb testing.TB, resp *model.Response) {
|
||||
tb.Helper()
|
||||
checkHTTPStatus(tb, resp, http.StatusNotImplemented)
|
||||
|
||||
@@ -22,6 +22,14 @@ func (api *API) InitRemoteCluster() {
|
||||
api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData, handlerParamFileAPI)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage, handlerParamFileAPI)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.RemoteCluster.Handle("", api.APISessionRequired(getRemoteClusters)).Methods("GET")
|
||||
api.BaseRoutes.RemoteCluster.Handle("", api.APISessionRequired(createRemoteCluster)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/accept_invite", api.APISessionRequired(remoteClusterAcceptInvite)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}/generate_invite", api.APISessionRequired(generateRemoteClusterInvite)).Methods("POST")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteCluster)).Methods("GET")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(patchRemoteCluster)).Methods("PATCH")
|
||||
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteRemoteCluster)).Methods("DELETE")
|
||||
}
|
||||
|
||||
func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -293,3 +301,359 @@ func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getRemoteClusters(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
|
||||
filter := model.RemoteClusterQueryFilter{
|
||||
ExcludeOffline: c.Params.ExcludeOffline,
|
||||
InChannel: c.Params.InChannel,
|
||||
NotInChannel: c.Params.NotInChannel,
|
||||
Topic: c.Params.Topic,
|
||||
CreatorId: c.Params.CreatorId,
|
||||
OnlyConfirmed: c.Params.OnlyConfirmed,
|
||||
PluginID: c.Params.PluginId,
|
||||
OnlyPlugins: c.Params.OnlyPlugins,
|
||||
ExcludePlugins: c.Params.ExcludePlugins,
|
||||
}
|
||||
|
||||
rcs, appErr := c.App.GetAllRemoteClusters(c.Params.Page, c.Params.PerPage, filter)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
for _, rc := range rcs {
|
||||
rc.Sanitize()
|
||||
}
|
||||
|
||||
b, err := json.Marshal(rcs)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getRemoteClusters", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func createRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("createRemoteCluster", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
var rcWithTeamAndPassword model.RemoteClusterWithPassword
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&rcWithTeamAndPassword); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("remoteCluster", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if rcWithTeamAndPassword.Password == "" {
|
||||
c.SetInvalidParam("password")
|
||||
return
|
||||
}
|
||||
|
||||
url := c.App.GetSiteURL()
|
||||
if url == "" {
|
||||
c.Err = model.NewAppError("createRemoteCluster", "api.get_site_url_error", nil, "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
if rcWithTeamAndPassword.DisplayName == "" {
|
||||
rcWithTeamAndPassword.DisplayName = rcWithTeamAndPassword.Name
|
||||
}
|
||||
|
||||
rc := &model.RemoteCluster{
|
||||
Name: rcWithTeamAndPassword.Name,
|
||||
DisplayName: rcWithTeamAndPassword.DisplayName,
|
||||
SiteURL: model.SiteURLPending + model.NewId(),
|
||||
Token: model.NewId(),
|
||||
CreatorId: c.AppContext.Session().UserId,
|
||||
}
|
||||
|
||||
audit.AddEventParameterAuditable(auditRec, "remotecluster", rc)
|
||||
|
||||
rcSaved, appErr := c.App.AddRemoteCluster(rc)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
rcSaved.Sanitize()
|
||||
|
||||
inviteCode, iErr := c.App.CreateRemoteClusterInvite(rcSaved.RemoteId, url, rcSaved.Token, rcWithTeamAndPassword.Password)
|
||||
if iErr != nil {
|
||||
c.Err = iErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(rcSaved)
|
||||
auditRec.AddEventObjectType("remotecluster")
|
||||
|
||||
b, err := json.Marshal(model.RemoteClusterWithInvite{RemoteCluster: rcSaved, Invite: inviteCode})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("createRemoteCluster", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func remoteClusterAcceptInvite(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
|
||||
c.SetPermissionError(model.PermissionManageSecureConnections)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure remote cluster service is enabled.
|
||||
rcs, appErr := c.App.GetRemoteClusterService()
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("remoteClusterAcceptInvite", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
var rcAcceptInvite model.RemoteClusterAcceptInvite
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&rcAcceptInvite); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("remoteCluster", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
audit.AddEventParameter(auditRec, "name", rcAcceptInvite.Name)
|
||||
audit.AddEventParameter(auditRec, "display_name", rcAcceptInvite.DisplayName)
|
||||
|
||||
if rcAcceptInvite.DisplayName == "" {
|
||||
rcAcceptInvite.DisplayName = rcAcceptInvite.Name
|
||||
}
|
||||
|
||||
invite, dErr := c.App.DecryptRemoteClusterInvite(rcAcceptInvite.Invite, rcAcceptInvite.Password)
|
||||
if dErr != nil {
|
||||
c.Err = dErr
|
||||
return
|
||||
}
|
||||
|
||||
audit.AddEventParameter(auditRec, "site_url", invite.SiteURL)
|
||||
|
||||
url := c.App.GetSiteURL()
|
||||
if url == "" {
|
||||
c.Err = model.NewAppError("remoteClusterAcceptInvite", "api.get_site_url_error", nil, "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
rc, aErr := rcs.AcceptInvitation(invite, rcAcceptInvite.Name, rcAcceptInvite.DisplayName, c.AppContext.Session().UserId, url)
|
||||
if aErr != nil {
|
||||
c.Err = model.NewAppError("remoteClusterAcceptInvite", "api.remote_cluster.accept_invitation_error", nil, "", http.StatusInternalServerError).Wrap(aErr)
|
||||
if appErr, ok := aErr.(*model.AppError); ok {
|
||||
c.Err = appErr
|
||||
}
|
||||
return
|
||||
}
|
||||
rc.Sanitize()
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(rc)
|
||||
auditRec.AddEventObjectType("remotecluster")
|
||||
|
||||
b, err := json.Marshal(rc)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("remoteClusterAcceptInvite", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func generateRemoteClusterInvite(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
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("generateRemoteClusterInvite", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
|
||||
|
||||
props := model.MapFromJSON(r.Body)
|
||||
password := props["password"]
|
||||
if password == "" {
|
||||
c.SetInvalidParam("password")
|
||||
return
|
||||
}
|
||||
|
||||
url := c.App.GetSiteURL()
|
||||
if url == "" {
|
||||
c.Err = model.NewAppError("generateRemoteClusterInvite", "api.get_site_url_error", nil, "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
rc, appErr := c.App.GetRemoteCluster(c.Params.RemoteId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
inviteCode, invErr := c.App.CreateRemoteClusterInvite(rc.RemoteId, url, rc.Token, password)
|
||||
if invErr != nil {
|
||||
c.Err = invErr
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(inviteCode))
|
||||
}
|
||||
|
||||
func getRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
|
||||
c.SetPermissionError(model.PermissionManageSecureConnections)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireRemoteId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// make sure remote cluster service is enabled.
|
||||
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := c.App.GetRemoteCluster(c.Params.RemoteId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
rc.Sanitize()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(rc); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) {
|
||||
c.SetPermissionError(model.PermissionManageSecureConnections)
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireRemoteId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// make sure remote cluster service is enabled.
|
||||
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
var patch model.RemoteClusterPatch
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("remotecluster", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("patchRemoteCluster", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
|
||||
audit.AddEventParameterAuditable(auditRec, "remotecluster_patch", &patch)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
orc, err := c.App.GetRemoteCluster(c.Params.RemoteId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(orc)
|
||||
auditRec.AddEventObjectType("remotecluster")
|
||||
|
||||
updatedRC, err := c.App.PatchRemoteCluster(c.Params.RemoteId, &patch)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(updatedRC)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updatedRC); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRemoteCluster(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
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("deleteRemoteCluster", audit.Fail)
|
||||
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
orc, err := c.App.GetRemoteCluster(c.Params.RemoteId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(orc)
|
||||
auditRec.AddEventObjectType("remotecluster")
|
||||
|
||||
deleted, err := c.App.DeleteRemoteCluster(c.Params.RemoteId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !deleted {
|
||||
c.Err = model.NewAppError("deleteRemoteCluster", "api.remote_cluster.cluster_not_deleted", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
559
server/channels/api4/remote_cluster_test.go
Обычный файл
559
server/channels/api4/remote_cluster_test.go
Обычный файл
@@ -0,0 +1,559 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetRemoteClusters(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()
|
||||
rcs, resp, err := th.SystemAdminClient.GetRemoteClusters(context.Background(), 0, 999999, model.RemoteClusterQueryFilter{})
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rcs)
|
||||
})
|
||||
|
||||
th := setupForSharedChannels(t)
|
||||
defer th.TearDown()
|
||||
|
||||
newRCs := []*model.RemoteCluster{
|
||||
{
|
||||
RemoteId: model.NewId(),
|
||||
Name: "remote1",
|
||||
SiteURL: "http://example1.com",
|
||||
CreatorId: th.SystemAdminUser.Id,
|
||||
Token: model.NewId(),
|
||||
RemoteToken: model.NewId(),
|
||||
},
|
||||
{
|
||||
RemoteId: model.NewId(),
|
||||
Name: "remote2",
|
||||
SiteURL: "http://example2.com",
|
||||
CreatorId: th.SystemAdminUser.Id,
|
||||
},
|
||||
{
|
||||
RemoteId: model.NewId(),
|
||||
Name: "remote3",
|
||||
SiteURL: "http://example3.com",
|
||||
CreatorId: th.SystemAdminUser.Id,
|
||||
PluginID: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, rc := range newRCs {
|
||||
_, appErr := th.App.AddRemoteCluster(rc)
|
||||
require.Nil(t, appErr)
|
||||
}
|
||||
|
||||
t.Run("The returned data should be sanitized", func(t *testing.T) {
|
||||
rcs, resp, err := th.SystemAdminClient.GetRemoteClusters(context.Background(), 0, 999999, model.RemoteClusterQueryFilter{})
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, rcs[0].Name, "remote")
|
||||
require.Zero(t, rcs[0].Token)
|
||||
require.Zero(t, rcs[0].RemoteToken)
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Client *model.Client4
|
||||
Page int
|
||||
PerPage int
|
||||
Filter model.RemoteClusterQueryFilter
|
||||
ExpectedStatusCode int
|
||||
ExpectedError bool
|
||||
ExpectedNames []string
|
||||
}{
|
||||
{
|
||||
Name: "Should reject if the user has not sufficient permissions",
|
||||
Client: th.Client,
|
||||
Page: 0,
|
||||
PerPage: 999999,
|
||||
Filter: model.RemoteClusterQueryFilter{},
|
||||
ExpectedStatusCode: 403,
|
||||
ExpectedError: true,
|
||||
ExpectedNames: []string{},
|
||||
},
|
||||
{
|
||||
Name: "Should return all remote clusters",
|
||||
Client: th.SystemAdminClient,
|
||||
Page: 0,
|
||||
PerPage: 999999,
|
||||
Filter: model.RemoteClusterQueryFilter{},
|
||||
ExpectedStatusCode: 200,
|
||||
ExpectedError: false,
|
||||
ExpectedNames: []string{"remote1", "remote2", "remote3"},
|
||||
},
|
||||
{
|
||||
Name: "Should return all remote clusters but those belonging to plugins",
|
||||
Client: th.SystemAdminClient,
|
||||
Page: 0,
|
||||
PerPage: 999999,
|
||||
Filter: model.RemoteClusterQueryFilter{ExcludePlugins: true},
|
||||
ExpectedStatusCode: 200,
|
||||
ExpectedError: false,
|
||||
ExpectedNames: []string{"remote1", "remote2"},
|
||||
},
|
||||
{
|
||||
Name: "Should return only remote clusters belonging to plugins",
|
||||
Client: th.SystemAdminClient,
|
||||
Page: 0,
|
||||
PerPage: 999999,
|
||||
Filter: model.RemoteClusterQueryFilter{OnlyPlugins: true},
|
||||
ExpectedStatusCode: 200,
|
||||
ExpectedError: false,
|
||||
ExpectedNames: []string{"remote3"},
|
||||
},
|
||||
{
|
||||
Name: "Should work as a paginated endpoint",
|
||||
Client: th.SystemAdminClient,
|
||||
Page: 1,
|
||||
PerPage: 1,
|
||||
Filter: model.RemoteClusterQueryFilter{},
|
||||
ExpectedStatusCode: 200,
|
||||
ExpectedError: false,
|
||||
ExpectedNames: []string{"remote2"},
|
||||
},
|
||||
{
|
||||
Name: "Should return an empty set with a successful status",
|
||||
Client: th.SystemAdminClient,
|
||||
Page: 0,
|
||||
PerPage: 999999,
|
||||
Filter: model.RemoteClusterQueryFilter{InChannel: model.NewId()},
|
||||
ExpectedStatusCode: 200,
|
||||
ExpectedError: false,
|
||||
ExpectedNames: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
rcs, resp, err := tc.Client.GetRemoteClusters(context.Background(), tc.Page, tc.PerPage, tc.Filter)
|
||||
checkHTTPStatus(t, resp, tc.ExpectedStatusCode)
|
||||
if tc.ExpectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Len(t, rcs, len(tc.ExpectedNames))
|
||||
names := []string{}
|
||||
for _, rc := range rcs {
|
||||
names = append(names, rc.Name)
|
||||
}
|
||||
require.ElementsMatch(t, tc.ExpectedNames, names)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRemoteCluster(t *testing.T) {
|
||||
rcWithTeamAndPassword := &model.RemoteClusterWithPassword{
|
||||
RemoteCluster: &model.RemoteCluster{
|
||||
Name: "remotecluster",
|
||||
SiteURL: "http://example.com",
|
||||
Token: model.NewId(),
|
||||
},
|
||||
Password: "mysupersecret",
|
||||
}
|
||||
|
||||
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
rcWithInvite, resp, err := th.SystemAdminClient.CreateRemoteCluster(context.Background(), rcWithTeamAndPassword)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rcWithInvite)
|
||||
})
|
||||
|
||||
th := setupForSharedChannels(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) {
|
||||
rcWithInvite, resp, err := th.Client.CreateRemoteCluster(context.Background(), rcWithTeamAndPassword)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rcWithInvite)
|
||||
})
|
||||
|
||||
t.Run("Should not work if the siteURL is not set in the configuration", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "" })
|
||||
rcWithInvite, resp, err := th.SystemAdminClient.CreateRemoteCluster(context.Background(), rcWithTeamAndPassword)
|
||||
CheckUnprocessableEntityStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rcWithInvite)
|
||||
})
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
|
||||
|
||||
t.Run("Should enforce the presence of the password", func(t *testing.T) {
|
||||
// clean the password and check the response
|
||||
rcWithTeamAndPassword.Password = ""
|
||||
|
||||
rcWithInvite, resp, err := th.SystemAdminClient.CreateRemoteCluster(context.Background(), rcWithTeamAndPassword)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rcWithInvite)
|
||||
|
||||
// reset password for the next tests
|
||||
rcWithTeamAndPassword.Password = "mysupersecret"
|
||||
})
|
||||
|
||||
t.Run("Should return a sanitized remote cluster and its invite", func(t *testing.T) {
|
||||
rcWithInvite, resp, err := th.SystemAdminClient.CreateRemoteCluster(context.Background(), rcWithTeamAndPassword)
|
||||
CheckCreatedStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, rcWithTeamAndPassword.Name, rcWithInvite.RemoteCluster.Name)
|
||||
require.NotZero(t, rcWithInvite.Invite)
|
||||
require.Zero(t, rcWithInvite.RemoteCluster.Token)
|
||||
require.Zero(t, rcWithInvite.RemoteCluster.RemoteToken)
|
||||
|
||||
rc, appErr := th.App.GetRemoteCluster(rcWithInvite.RemoteCluster.RemoteId)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, rcWithTeamAndPassword.Name, rc.Name)
|
||||
|
||||
rci, appErr := th.App.DecryptRemoteClusterInvite(rcWithInvite.Invite, rcWithTeamAndPassword.Password)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, rc.RemoteId, rci.RemoteId)
|
||||
require.Equal(t, rc.RemoteToken, rci.Token)
|
||||
require.Equal(t, th.App.GetSiteURL(), rci.SiteURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoteClusterAcceptinvite(t *testing.T) {
|
||||
rcAcceptInvite := &model.RemoteClusterAcceptInvite{
|
||||
Name: "remotecluster",
|
||||
Invite: "myinvitecode",
|
||||
Password: "mysupersecret",
|
||||
}
|
||||
|
||||
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
rc, resp, err := th.SystemAdminClient.RemoteClusterAcceptInvite(context.Background(), rcAcceptInvite)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
th := setupForSharedChannels(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteId := model.NewId()
|
||||
invite := &model.RemoteClusterInvite{
|
||||
RemoteId: remoteId,
|
||||
SiteURL: "http://localhost:8065",
|
||||
Token: "token",
|
||||
}
|
||||
password := "mysupersecret"
|
||||
encrypted, err := invite.Encrypt(password)
|
||||
require.NoError(t, err)
|
||||
encoded := base64.URLEncoding.EncodeToString(encrypted)
|
||||
rcAcceptInvite.Invite = encoded
|
||||
|
||||
t.Run("Should not work if the siteURL is not set in the configuration", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "" })
|
||||
rc, resp, err := th.SystemAdminClient.RemoteClusterAcceptInvite(context.Background(), rcAcceptInvite)
|
||||
CheckUnprocessableEntityStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
|
||||
|
||||
t.Run("should fail if the parameters are not valid", func(t *testing.T) {
|
||||
rcAcceptInvite.Name = ""
|
||||
defer func() { rcAcceptInvite.Name = "remotecluster" }()
|
||||
|
||||
rc, resp, err := th.SystemAdminClient.RemoteClusterAcceptInvite(context.Background(), rcAcceptInvite)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
t.Run("should fail with the correct status code if the invite returns an app error", func(t *testing.T) {
|
||||
rcAcceptInvite.Invite = "malformedinvite"
|
||||
// reset the invite after
|
||||
defer func() { rcAcceptInvite.Invite = encoded }()
|
||||
|
||||
rc, resp, err := th.SystemAdminClient.RemoteClusterAcceptInvite(context.Background(), rcAcceptInvite)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
t.Run("should not work if the user doesn't have the right permissions", func(t *testing.T) {
|
||||
rc, resp, err := th.Client.RemoteClusterAcceptInvite(context.Background(), rcAcceptInvite)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
t.Run("should return a sanitized remote cluster if the action succeeds", func(t *testing.T) {
|
||||
t.Skip("Requires server2server communication: ToBeImplemented")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateRemoteClusterInvite(t *testing.T) {
|
||||
password := "mysupersecret"
|
||||
|
||||
newRC := &model.RemoteCluster{
|
||||
Name: "remotecluster",
|
||||
SiteURL: "http://example.com",
|
||||
Token: model.NewId(),
|
||||
}
|
||||
|
||||
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
|
||||
inviteCode, resp, err := th.SystemAdminClient.GenerateRemoteClusterInvite(context.Background(), rc.RemoteId, password)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Zero(t, inviteCode)
|
||||
})
|
||||
|
||||
th := setupForSharedChannels(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
|
||||
t.Run("Should not work if the siteURL is not set in the configuration", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "" })
|
||||
inviteCode, resp, err := th.SystemAdminClient.GenerateRemoteClusterInvite(context.Background(), rc.RemoteId, password)
|
||||
CheckUnprocessableEntityStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, inviteCode)
|
||||
})
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
|
||||
|
||||
t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) {
|
||||
inviteCode, resp, err := th.Client.GenerateRemoteClusterInvite(context.Background(), rc.RemoteId, password)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, inviteCode)
|
||||
})
|
||||
|
||||
t.Run("should not work if the remote cluster doesn't exist", func(t *testing.T) {
|
||||
inviteCode, resp, err := th.SystemAdminClient.GenerateRemoteClusterInvite(context.Background(), model.NewId(), password)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, inviteCode)
|
||||
})
|
||||
|
||||
t.Run("should not work if the password has been provided", func(t *testing.T) {
|
||||
inviteCode, resp, err := th.SystemAdminClient.GenerateRemoteClusterInvite(context.Background(), rc.RemoteId, "")
|
||||
CheckBadRequestStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, inviteCode)
|
||||
})
|
||||
|
||||
t.Run("should generate a valid invite code", func(t *testing.T) {
|
||||
inviteCode, resp, err := th.SystemAdminClient.GenerateRemoteClusterInvite(context.Background(), rc.RemoteId, password)
|
||||
CheckCreatedStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, inviteCode)
|
||||
|
||||
invite, appErr := th.App.DecryptRemoteClusterInvite(inviteCode, password)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, rc.RemoteId, invite.RemoteId)
|
||||
require.Equal(t, rc.Token, invite.Token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRemoteCluster(t *testing.T) {
|
||||
newRC := &model.RemoteCluster{
|
||||
Name: "remotecluster",
|
||||
SiteURL: "http://example.com",
|
||||
Token: model.NewId(),
|
||||
}
|
||||
|
||||
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
require.NotZero(t, rc.Token)
|
||||
|
||||
fetchedRC, resp, err := th.SystemAdminClient.GetRemoteCluster(context.Background(), rc.RemoteId)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, fetchedRC)
|
||||
})
|
||||
|
||||
th := setupForSharedChannels(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
|
||||
t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) {
|
||||
fetchedRC, resp, err := th.Client.GetRemoteCluster(context.Background(), rc.RemoteId)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, fetchedRC)
|
||||
})
|
||||
|
||||
t.Run("should return not found if the id doesn't exist", func(t *testing.T) {
|
||||
fetchedRC, resp, err := th.SystemAdminClient.GetRemoteCluster(context.Background(), model.NewId())
|
||||
CheckNotFoundStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, fetchedRC)
|
||||
})
|
||||
|
||||
t.Run("should return a sanitized remote cluster", func(t *testing.T) {
|
||||
fetchedRC, resp, err := th.SystemAdminClient.GetRemoteCluster(context.Background(), rc.RemoteId)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, rc.RemoteId, fetchedRC.RemoteId)
|
||||
require.Empty(t, fetchedRC.Token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchRemoteCluster(t *testing.T) {
|
||||
newRC := &model.RemoteCluster{
|
||||
Name: "remotecluster",
|
||||
DisplayName: "initialvalue",
|
||||
SiteURL: "http://example.com",
|
||||
Token: model.NewId(),
|
||||
}
|
||||
|
||||
rcp := &model.RemoteClusterPatch{DisplayName: model.NewString("different value")}
|
||||
|
||||
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
|
||||
patchedRC, resp, err := th.SystemAdminClient.PatchRemoteCluster(context.Background(), rc.RemoteId, rcp)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, patchedRC)
|
||||
})
|
||||
|
||||
th := setupForSharedChannels(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
|
||||
t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) {
|
||||
patchedRC, resp, err := th.Client.PatchRemoteCluster(context.Background(), rc.RemoteId, rcp)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, patchedRC)
|
||||
})
|
||||
|
||||
t.Run("should not work if the remote cluster is nonexistent", func(t *testing.T) {
|
||||
patchedRC, resp, err := th.SystemAdminClient.PatchRemoteCluster(context.Background(), model.NewId(), rcp)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, patchedRC)
|
||||
})
|
||||
|
||||
t.Run("should correctly patch the remote cluster", func(t *testing.T) {
|
||||
rcp := &model.RemoteClusterPatch{DisplayName: model.NewString("patched!")}
|
||||
|
||||
patchedRC, resp, err := th.SystemAdminClient.PatchRemoteCluster(context.Background(), rc.RemoteId, rcp)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "patched!", patchedRC.DisplayName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteRemoteCluster(t *testing.T) {
|
||||
newRC := &model.RemoteCluster{
|
||||
Name: "remotecluster",
|
||||
DisplayName: "initialvalue",
|
||||
SiteURL: "http://example.com",
|
||||
Token: model.NewId(),
|
||||
}
|
||||
|
||||
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
|
||||
resp, err := th.SystemAdminClient.DeleteRemoteCluster(context.Background(), rc.RemoteId)
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
th := setupForSharedChannels(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
newRC.CreatorId = th.SystemAdminUser.Id
|
||||
|
||||
rc, appErr := th.App.AddRemoteCluster(newRC)
|
||||
require.Nil(t, appErr)
|
||||
require.NotZero(t, rc.RemoteId)
|
||||
|
||||
t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) {
|
||||
resp, err := th.Client.DeleteRemoteCluster(context.Background(), rc.RemoteId)
|
||||
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.DeleteRemoteCluster(context.Background(), model.NewId())
|
||||
CheckNotFoundStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("should correctly delete the remote cluster", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.DeleteRemoteCluster(context.Background(), rc.RemoteId)
|
||||
CheckNoContentStatus(t, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
deletedRC, err := th.App.GetRemoteCluster(rc.RemoteId)
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
require.Empty(t, deletedRC)
|
||||
})
|
||||
|
||||
t.Run("should return not found if the remote cluster is already deleted", func(t *testing.T) {
|
||||
resp, err := th.SystemAdminClient.DeleteRemoteCluster(context.Background(), rc.RemoteId)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
Ссылка в новой задаче
Block a user