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>
This commit is contained in:
Miguel de la Cruz
2024-07-04 10:35:26 +02:00
committed by GitHub
parent cc5e87ae24
commit 809ad4f76d
29 changed files with 1702 additions and 77 deletions

View File

@@ -40,6 +40,7 @@ build-v4: node_modules playbooks
@cat $(V4_SRC)/roles.yaml >> $(V4_YAML)
@cat $(V4_SRC)/schemes.yaml >> $(V4_YAML)
@cat $(V4_SRC)/service_terms.yaml >> $(V4_YAML)
@cat $(V4_SRC)/remoteclusters.yaml >> $(V4_YAML)
@cat $(V4_SRC)/sharedchannels.yaml >> $(V4_YAML)
@cat $(V4_SRC)/reactions.yaml >> $(V4_YAML)
@cat $(V4_SRC)/actions.yaml >> $(V4_YAML)

View File

@@ -3485,6 +3485,39 @@ components:
remote_id:
description: Id of the remote cluster where the shared channel is homed
type: string
RemoteCluster:
type: object
properties:
remote_id:
type: string
remote_team_id:
type: string
name:
type: string
display_name:
type: string
site_url:
description: URL of the remote cluster
type: string
create_at:
description: Time in milliseconds that the remote cluster was created
type: integer
last_ping_at:
description: Time in milliseconds when the last ping to the remote cluster was run
type: integer
token:
type: string
remote_token:
type: string
topics:
type: string
creator_id:
type: string
plugin_id:
type: string
options:
description: A bitmask with a set of option flags
type: integer
RemoteClusterInfo:
type: object
properties:

View File

@@ -598,6 +598,7 @@ x-tagGroups:
- roles
- schemes
- integration_actions
- remote clusters
- shared channels
- terms of service
- imports

View File

@@ -0,0 +1,282 @@
"/api/v4/remotecluster":
get:
tags:
- remote clusters
summary: Get a list of remote clusters.
description: |
Get a list of remote clusters.
##### Permissions
`manage_secure_connections`
operationId: GetRemoteClusters
parameters:
- name: page
in: query
description: The page to select
schema:
type: integer
- name: per_page
in: query
description: The number of remote clusters per page
schema:
type: integer
- name: exclude_offline
in: query
description: Exclude offline remote clusters
schema:
type: boolean
- name: in_channel
in: query
description: Select remote clusters in channel
schema:
type: string
- name: not_in_channel
in: query
description: Select remote clusters not in this channel
schema:
type: string
- name: only_confirmed
in: query
description: Select only remote clusters already confirmed
schema:
type: boolean
- name: only_plugins
in: query
description: Select only remote clusters that belong to a plugin
schema:
type: boolean
- name: exclude_plugins
in: query
description: Select only remote clusters that don't belong to a plugin
schema:
type: boolean
responses:
"200":
description: Remote clusters fetch successful. Result might be empty.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RemoteCluster"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
post:
tags:
- remote clusters
summary: Create a new remote cluster.
description: |
Create a new remote cluster and generate an invite code.
##### Permissions
`manage_secure_connections`
operationId: CreateRemoteCluster
requestBody:
content:
application/json:
schema:
type: object
required:
- name
- password
properties:
name:
type: string
display_name:
type: string
password:
type: string
description: The password to use in the invite code.
responses:
"201":
description: Remote cluster creation successful
content:
application/json:
schema:
type: object
properties:
remote_cluster:
$ref: "#/components/schemas/RemoteCluster"
invite:
type: string
description: The encrypted invite for the newly created remote cluster
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"/api/v4/remotecluster/{remote_id}":
get:
tags:
- remote clusters
summary: Get a remote cluster.
description: |
Get the Remote Cluster details from the provided id string.
##### Permissions
`manage_secure_connections`
operationId: GetRemoteCluster
parameters:
- name: remote_id
in: path
description: Remote Cluster GUID
required: true
schema:
type: string
responses:
"200":
description: Remote Cluster retrieval successful
content:
application/json:
schema:
$ref: "#/components/schemas/RemoteCluster"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
patch:
tags:
- remote clusters
summary: Patch a remote cluster.
description: |
Partially update a Remote Cluster by providing only the fields you want to update. Ommited fields will not be updated.
##### Permissions
`manage_secure_connections`
operationId: PatchRemoteCluster
parameters:
- name: remote_id
in: path
description: Remote Cluster GUID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
display_name:
type: string
responses:
"200":
description: Remote Cluster patch successful
content:
application/json:
schema:
$ref: "#/components/schemas/RemoteCluster"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
delete:
tags:
- remote clusters
summary: Delete a remote cluster.
description: |
Deletes a Remote Cluster.
##### Permissions
`manage_secure_connections`
operationId: DeleteRemoteCluster
parameters:
- name: remote_id
in: path
description: Remote Cluster GUID
required: true
schema:
type: string
responses:
"204":
description: Remote Cluster deletion successful
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"/api/v4/remotecluster/{remote_id}/generate_invite":
post:
tags:
- remote clusters
summary: Generate invite code.
description: |
Generates an invite code for a given remote cluster.
##### Permissions
`manage_secure_connections`
operationId: GenerateRemoteClusterInvite
requestBody:
content:
application/json:
schema:
type: object
required:
- password
properties:
password:
type: string
description: The password to encrypt the invite code with.
responses:
"201":
description: Invite code generated
content:
application/json:
schema:
type: string
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"/api/v4/remotecluster/accept_invite":
post:
tags:
- remote clusters
summary: Accept a remote cluster invite code.
description: |
Accepts a remote cluster invite code.
##### Permissions
`manage_secure_connections`
operationId: AcceptRemoteClusterInvite
requestBody:
content:
application/json:
schema:
type: object
required:
- invite
- name
- password
properties:
invite:
type: string
name:
type: string
display_name:
type: string
password:
type: string
description: The password to decrypt the invite code.
responses:
"201":
description: Invite successfully accepted
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/RemoteCluster"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
})
}

View File

@@ -533,6 +533,7 @@ type AppIface interface {
CreatePost(c request.CTX, post *model.Post, channel *model.Channel, triggerWebhooks, setOnline bool) (savedPost *model.Post, err *model.AppError)
CreatePostAsUser(c request.CTX, post *model.Post, currentSessionId string, setOnline bool) (*model.Post, *model.AppError)
CreatePostMissingChannel(c request.CTX, post *model.Post, triggerWebhooks bool, setOnline bool) (*model.Post, *model.AppError)
CreateRemoteClusterInvite(remoteId, siteURL, token, password string) (string, *model.AppError)
CreateRetentionPolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError)
CreateRole(role *model.Role) (*model.Role, *model.AppError)
CreateSamlRelayToken(extra string) (*model.Token, *model.AppError)
@@ -555,6 +556,7 @@ type AppIface interface {
DeactivateGuests(c request.CTX) *model.AppError
DeactivateMfa(userID string) *model.AppError
DeauthorizeOAuthAppForUser(c request.CTX, userID, appID string) *model.AppError
DecryptRemoteClusterInvite(inviteCode, password string) (*model.RemoteClusterInvite, *model.AppError)
DeleteAcknowledgementForPost(c request.CTX, postID, userID string) *model.AppError
DeleteAllExpiredPluginKeys() *model.AppError
DeleteAllKeysForPlugin(pluginID string) *model.AppError
@@ -626,7 +628,7 @@ type AppIface interface {
GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError)
GetAllPrivateTeams() ([]*model.Team, *model.AppError)
GetAllPublicTeams() ([]*model.Team, *model.AppError)
GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError)
GetAllRemoteClusters(page, perPage int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError)
GetAllRoles() ([]*model.Role, *model.AppError)
GetAllTeams() ([]*model.Team, *model.AppError)
GetAllTeamsPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, *model.AppError)
@@ -973,6 +975,7 @@ type AppIface interface {
PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError)
PatchChannelMembersNotifyProps(c request.CTX, members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, *model.AppError)
PatchPost(c request.CTX, postID string, patch *model.PostPatch) (*model.Post, *model.AppError)
PatchRemoteCluster(rcId string, patch *model.RemoteClusterPatch) (*model.RemoteCluster, *model.AppError)
PatchRetentionPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError)
PatchRole(role *model.Role, patch *model.RolePatch) (*model.Role, *model.AppError)
PatchScheme(scheme *model.Scheme, patch *model.SchemePatch) (*model.Scheme, *model.AppError)

View File

@@ -2544,6 +2544,28 @@ func (a *OpenTracingAppLayer) CreatePostMissingChannel(c request.CTX, post *mode
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateRemoteClusterInvite(remoteId string, siteURL string, token string, password string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateRemoteClusterInvite")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateRemoteClusterInvite(remoteId, siteURL, token, password)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateRetentionPolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateRetentionPolicy")
@@ -3050,6 +3072,28 @@ func (a *OpenTracingAppLayer) DeauthorizeOAuthAppForUser(c request.CTX, userID s
return resultVar0
}
func (a *OpenTracingAppLayer) DecryptRemoteClusterInvite(inviteCode string, password string) (*model.RemoteClusterInvite, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DecryptRemoteClusterInvite")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DecryptRemoteClusterInvite(inviteCode, password)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DefaultChannelNames(c request.CTX) []string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DefaultChannelNames")
@@ -5023,7 +5067,7 @@ func (a *OpenTracingAppLayer) GetAllPublicTeams() ([]*model.Team, *model.AppErro
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
func (a *OpenTracingAppLayer) GetAllRemoteClusters(page int, perPage int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllRemoteClusters")
@@ -5035,7 +5079,7 @@ func (a *OpenTracingAppLayer) GetAllRemoteClusters(filter model.RemoteClusterQue
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllRemoteClusters(filter)
resultVar0, resultVar1 := a.app.GetAllRemoteClusters(page, perPage, filter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
@@ -13376,6 +13420,28 @@ func (a *OpenTracingAppLayer) PatchPost(c request.CTX, postID string, patch *mod
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchRemoteCluster(rcId string, patch *model.RemoteClusterPatch) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchRemoteCluster(rcId, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchRetentionPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchRetentionPolicy")

View File

@@ -88,7 +88,7 @@ func handleContentSync(ps *PlatformService, syncService SharedChannelServiceIFac
OnlyConfirmed: true,
RequireOptions: model.BitflagOptionAutoShareDMs,
}
remotes, err := ps.Store.RemoteCluster().GetAll(filter) // empty list returned if none found, no error
remotes, err := ps.Store.RemoteCluster().GetAll(0, 999999, filter) // empty list returned if none found, no error
if err != nil {
return fmt.Errorf("cannot fetch remote clusters: %w", err)
}

View File

@@ -5,6 +5,7 @@ package app
import (
"database/sql"
"encoding/base64"
"net/http"
"github.com/pkg/errors"
@@ -100,6 +101,22 @@ func (a *App) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *
return rc, nil
}
func (a *App) PatchRemoteCluster(rcId string, patch *model.RemoteClusterPatch) (*model.RemoteCluster, *model.AppError) {
rc, err := a.GetRemoteCluster(rcId)
if err != nil {
return nil, err
}
rc.Patch(patch)
updatedRC, err := a.UpdateRemoteCluster(rc)
if err != nil {
return nil, err
}
return updatedRC, nil
}
func (a *App) UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Update(rc)
if err != nil {
@@ -123,13 +140,18 @@ func (a *App) DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError
func (a *App) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Get(remoteClusterId)
if err != nil {
return nil, model.NewAppError("GetRemoteCluster", "api.remote_cluster.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, model.NewAppError("GetRemoteCluster", "api.remote_cluster.get.not_found", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetRemoteCluster", "api.remote_cluster.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return rc, nil
}
func (a *App) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
list, err := a.Srv().Store().RemoteCluster().GetAll(filter)
func (a *App) GetAllRemoteClusters(page, perPage int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
list, err := a.Srv().Store().RemoteCluster().GetAll(page*perPage, perPage, filter)
if err != nil {
return nil, model.NewAppError("GetAllRemoteClusters", "api.remote_cluster.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
@@ -159,3 +181,32 @@ func (a *App) GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace
}
return service, nil
}
func (a *App) CreateRemoteClusterInvite(remoteId, siteURL, token, password string) (string, *model.AppError) {
invite := &model.RemoteClusterInvite{
RemoteId: remoteId,
SiteURL: siteURL,
Token: token,
}
encrypted, err := invite.Encrypt(password)
if err != nil {
return "", model.NewAppError("CreateRemoteClusterInvite", "api.remote_cluster.encrypt_invite_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return base64.URLEncoding.EncodeToString(encrypted), nil
}
func (a *App) DecryptRemoteClusterInvite(inviteCode, password string) (*model.RemoteClusterInvite, *model.AppError) {
decoded, err := base64.URLEncoding.DecodeString(inviteCode)
if err != nil {
return nil, model.NewAppError("DecryptRemoteClusterInvite", "api.remote_cluster.base64_decode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
invite := &model.RemoteClusterInvite{}
if dErr := invite.Decrypt(decoded, password); dErr != nil {
return nil, model.NewAppError("DecryptRemoteClusterInvite", "api.remote_cluster.invite_decrypt_error", nil, "", http.StatusBadRequest).Wrap(dErr)
}
return invite, nil
}

View File

@@ -4,7 +4,6 @@
package slashcommands
import (
"encoding/base64"
"errors"
"fmt"
"strings"
@@ -144,19 +143,13 @@ func (rp *RemoteProvider) doCreate(a *app.App, args *model.CommandArgs, margs ma
}
// Display the encrypted invitation
invite := &model.RemoteClusterInvite{
RemoteId: rcSaved.RemoteId,
SiteURL: url,
Token: rcSaved.Token,
}
encrypted, err := invite.Encrypt(password)
inviteCode, err := a.CreateRemoteClusterInvite(rcSaved.RemoteId, url, rcSaved.Token, password)
if err != nil {
return responsef(args.T("api.command_remote.encrypt_invitation.error", map[string]any{"Error": err.Error()}))
}
encoded := base64.URLEncoding.EncodeToString(encrypted)
return responsef("##### " + args.T("api.command_remote.invitation_created") + "\n" +
args.T("api.command_remote.invite_summary", map[string]any{"Command": "/secure-connection accept", "Invitation": encoded, "SiteURL": invite.SiteURL}))
args.T("api.command_remote.invite_summary", map[string]any{"Command": "/secure-connection accept", "Invitation": inviteCode, "SiteURL": url}))
}
// doAccept accepts an invitation generated by a remote site.
@@ -182,14 +175,9 @@ func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs ma
}
// invite is encoded as base64 and encrypted
decoded, err := base64.URLEncoding.DecodeString(blob)
if err != nil {
return responsef(args.T("api.command_remote.decode_invitation.error", map[string]any{"Error": err.Error()}))
}
invite := &model.RemoteClusterInvite{}
err = invite.Decrypt(decoded, password)
if err != nil {
return responsef(args.T("api.command_remote.incorrect_password.error", map[string]any{"Error": err.Error()}))
invite, dErr := a.DecryptRemoteClusterInvite(blob, password)
if dErr != nil {
return responsef(args.T("api.command_remote.decode_invitation.error", map[string]any{"Error": dErr.Error()}))
}
rcs, _ := a.GetRemoteClusterService()
@@ -202,7 +190,7 @@ func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs ma
return responsef(args.T("api.command_remote.site_url_not_set"))
}
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, args.TeamId, url)
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, url)
if err != nil {
return responsef(args.T("api.command_remote.accept_invitation.error", map[string]any{"Error": err.Error()}))
}
@@ -231,7 +219,7 @@ func (rp *RemoteProvider) doRemove(a *app.App, args *model.CommandArgs, margs ma
// doStatus displays connection status for all remote clusters.
func (rp *RemoteProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
list, err := a.GetAllRemoteClusters(model.RemoteClusterQueryFilter{})
list, err := a.GetAllRemoteClusters(0, 999999, model.RemoteClusterQueryFilter{})
if err != nil {
responsef(args.T("api.command_remote.fetch_status.error", map[string]any{"Error": err.Error()}))
}
@@ -263,7 +251,7 @@ func getRemoteClusterAutocompleteListItems(a *app.App, includeOffline bool) ([]m
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: !includeOffline,
}
clusters, err := a.GetAllRemoteClusters(filter)
clusters, err := a.GetAllRemoteClusters(0, 999999, filter)
if err != nil || len(clusters) == 0 {
return []model.AutocompleteListItem{}, nil
}
@@ -284,7 +272,7 @@ func getRemoteClusterAutocompleteListItemsNotInChannel(a *app.App, channelID str
ExcludeOffline: !includeOffline,
NotInChannel: channelID,
}
all, err := a.GetAllRemoteClusters(filter)
all, err := a.GetAllRemoteClusters(0, 999999, filter)
if err != nil || len(all) == 0 {
return []model.AutocompleteListItem{}, nil
}

View File

@@ -7828,7 +7828,7 @@ func (s *OpenTracingLayerRemoteClusterStore) Get(remoteClusterId string) (*model
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
func (s *OpenTracingLayerRemoteClusterStore) GetAll(offset int, limit int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.GetAll")
s.Root.Store.SetContext(newCtx)
@@ -7837,7 +7837,7 @@ func (s *OpenTracingLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQu
}()
defer span.Finish()
result, err := s.RemoteClusterStore.GetAll(filter)
result, err := s.RemoteClusterStore.GetAll(offset, limit, filter)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)

View File

@@ -8909,11 +8909,11 @@ func (s *RetryLayerRemoteClusterStore) Get(remoteClusterId string) (*model.Remot
}
func (s *RetryLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
func (s *RetryLayerRemoteClusterStore) GetAll(offset int, limit int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.GetAll(filter)
result, err := s.RemoteClusterStore.GetAll(offset, limit, filter)
if err == nil {
return result, nil
}

View File

@@ -162,7 +162,14 @@ func (s sqlRemoteClusterStore) GetByPluginID(pluginID string) (*model.RemoteClus
return &rc, nil
}
func (s sqlRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
func (s sqlRemoteClusterStore) GetAll(offset, limit int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
if offset < 0 {
return nil, errors.New("offset must be a positive integer")
}
if limit < 0 {
return nil, errors.New("limit must be a positive integer")
}
query := s.getQueryBuilder().
Select(remoteClusterFields("rc")...).
From("RemoteClusters rc")
@@ -195,6 +202,10 @@ func (s sqlRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]
query = query.Where(sq.NotEq{"rc.PluginID": ""})
}
if filter.ExcludePlugins {
query = query.Where(sq.Eq{"rc.PluginID": ""})
}
if filter.RequireOptions != 0 {
query = query.Where(sq.NotEq{fmt.Sprintf("(rc.Options & %d)", filter.RequireOptions): 0})
}
@@ -208,6 +219,8 @@ func (s sqlRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]
query = query.Where(sq.Or{sq.Like{"rc.Topics": queryTopic}, sq.Eq{"rc.Topics": "*"}})
}
query = query.Offset(uint64(offset)).Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "remote_cluster_getall_tosql")

View File

@@ -533,7 +533,7 @@ type RemoteClusterStore interface {
Delete(remoteClusterId string) (bool, error)
Get(remoteClusterId string) (*model.RemoteCluster, error)
GetByPluginID(pluginID string) (*model.RemoteCluster, error)
GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error)
GetAll(offset, limit int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error)
UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error)
SetLastPingAt(remoteClusterId string) error
}

View File

@@ -72,9 +72,9 @@ func (_m *RemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster,
return r0, r1
}
// GetAll provides a mock function with given fields: filter
func (_m *RemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
ret := _m.Called(filter)
// GetAll provides a mock function with given fields: offset, limit, filter
func (_m *RemoteClusterStore) GetAll(offset int, limit int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
ret := _m.Called(offset, limit, filter)
if len(ret) == 0 {
panic("no return value specified for GetAll")
@@ -82,19 +82,19 @@ func (_m *RemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*
var r0 []*model.RemoteCluster
var r1 error
if rf, ok := ret.Get(0).(func(model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error)); ok {
return rf(filter)
if rf, ok := ret.Get(0).(func(int, int, model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error)); ok {
return rf(offset, limit, filter)
}
if rf, ok := ret.Get(0).(func(model.RemoteClusterQueryFilter) []*model.RemoteCluster); ok {
r0 = rf(filter)
if rf, ok := ret.Get(0).(func(int, int, model.RemoteClusterQueryFilter) []*model.RemoteCluster); ok {
r0 = rf(offset, limit, filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RemoteCluster)
}
}
if rf, ok := ret.Get(1).(func(model.RemoteClusterQueryFilter) error); ok {
r1 = rf(filter)
if rf, ok := ret.Get(1).(func(int, int, model.RemoteClusterQueryFilter) error); ok {
r1 = rf(offset, limit, filter)
} else {
r1 = ret.Error(1)
}

View File

@@ -238,6 +238,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
idsOnline := make([]string, 0)
idsShareTopic := make([]string, 0)
idsPlugin := make([]string, 0)
idsNotPlugin := make([]string, 0)
idsConfirmed := make([]string, 0)
for _, item := range data {
@@ -253,6 +254,8 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
}
if item.PluginID != "" {
idsPlugin = append(idsPlugin, saved.RemoteId)
} else {
idsNotPlugin = append(idsNotPlugin, saved.RemoteId)
}
if item.SiteURL != "" {
idsConfirmed = append(idsConfirmed, saved.RemoteId)
@@ -261,7 +264,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("GetAll", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{}
remotes, err := ss.RemoteCluster().GetAll(filter)
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure all the test data remotes were returned.
ids := getIds(remotes)
@@ -272,7 +275,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure all the online remotes were returned.
ids := getIds(remotes)
@@ -283,7 +286,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
filter := model.RemoteClusterQueryFilter{
Topic: "shared",
}
remotes, err := ss.RemoteCluster().GetAll(filter)
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure only correct topic returned
ids := getIds(remotes)
@@ -295,7 +298,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
ExcludeOffline: true,
Topic: "shared",
}
remotes, err := ss.RemoteCluster().GetAll(filter)
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure only online remotes were returned.
ids := getIds(remotes)
@@ -309,7 +312,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
filter := model.RemoteClusterQueryFilter{
CreatorId: userId,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure only correct creator returned
assert.Len(t, remotes, 3)
@@ -322,7 +325,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
filter := model.RemoteClusterQueryFilter{
OnlyConfirmed: true,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure only confirmed returned
for _, rc := range remotes {
@@ -337,7 +340,7 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
filter := model.RemoteClusterQueryFilter{
OnlyPlugins: true,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure only plugin remotes returned
for _, rc := range remotes {
@@ -348,6 +351,22 @@ func testRemoteClusterGetAll(t *testing.T, _ request.CTX, ss store.Store) {
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsPlugin)
})
t.Run("GetAll excluding plugins", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludePlugins: true,
}
remotes, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
// make sure only non plugin remotes returned
for _, rc := range remotes {
assert.Empty(t, rc.PluginID)
assert.False(t, rc.IsPlugin())
}
// make sure all of the non plugin remotes were returned.
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsNotPlugin)
})
}
func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.Store) {
@@ -411,7 +430,7 @@ func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.S
filter := model.RemoteClusterQueryFilter{
InChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 2, "channel 1 should have 2 remote clusters")
ids := getIds(list)
@@ -425,7 +444,7 @@ func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.S
ExcludeOffline: true,
InChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 1, "channel 1 should have 1 online remote clusters")
ids := getIds(list)
@@ -436,7 +455,7 @@ func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.S
filter := model.RemoteClusterQueryFilter{
InChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 2 should have 3 remote clusters")
ids := getIds(list)
@@ -448,7 +467,7 @@ func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.S
ExcludeOffline: true,
InChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 2, "channel 2 should have 2 online remote clusters")
ids := getIds(list)
@@ -459,7 +478,7 @@ func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.S
filter := model.RemoteClusterQueryFilter{
InChannel: channel3.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Empty(t, list, "channel 3 should have 0 remote clusters")
})
@@ -520,7 +539,7 @@ func testRemoteClusterGetAllNotInChannel(t *testing.T, rctx request.CTX, ss stor
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 1 should have 3 remote clusters that are not already members")
ids := getIds(list)
@@ -531,7 +550,7 @@ func testRemoteClusterGetAllNotInChannel(t *testing.T, rctx request.CTX, ss stor
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 2 should have 3 remote clusters that are not already members")
ids := getIds(list)
@@ -542,7 +561,7 @@ func testRemoteClusterGetAllNotInChannel(t *testing.T, rctx request.CTX, ss stor
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel3.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 4, "channel 3 should have 4 remote clusters that are not already members")
ids := getIds(list)
@@ -553,7 +572,7 @@ func testRemoteClusterGetAllNotInChannel(t *testing.T, rctx request.CTX, ss stor
filter := model.RemoteClusterQueryFilter{
NotInChannel: model.NewId(),
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
require.NoError(t, err)
require.Len(t, list, 5, "should have 5 remote clusters that are not already members")
ids := getIds(list)
@@ -605,7 +624,7 @@ func testRemoteClusterGetByTopic(t *testing.T, _ request.CTX, ss store.Store) {
filter := model.RemoteClusterQueryFilter{
Topic: tt.topic,
}
list, err := ss.RemoteCluster().GetAll(filter)
list, err := ss.RemoteCluster().GetAll(0, 999999, filter)
if tt.expectError {
assert.Errorf(t, err, "expected error for topic=%s", tt.topic)
} else {
@@ -653,7 +672,7 @@ func testRemoteClusterUpdateTopics(t *testing.T, _ request.CTX, ss store.Store)
}
func clearRemoteClusters(ss store.Store) error {
list, err := ss.RemoteCluster().GetAll(model.RemoteClusterQueryFilter{})
list, err := ss.RemoteCluster().GetAll(0, 999999, model.RemoteClusterQueryFilter{})
if err != nil {
return err
}

View File

@@ -7065,10 +7065,10 @@ func (s *TimerLayerRemoteClusterStore) Get(remoteClusterId string) (*model.Remot
return result, err
}
func (s *TimerLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
func (s *TimerLayerRemoteClusterStore) GetAll(offset int, limit int, filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.GetAll(filter)
result, err := s.RemoteClusterStore.GetAll(offset, limit, filter)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {

View File

@@ -93,6 +93,14 @@ type Params struct {
FilterHasMember string
IncludeChannelMemberCount string
OutgoingOAuthConnectionID string
ExcludeOffline bool
InChannel string
NotInChannel string
Topic string
CreatorId string
OnlyConfirmed bool
OnlyPlugins bool
ExcludePlugins bool
//Bookmarks
ChannelBookmarkId string
@@ -126,7 +134,11 @@ func ParamsFromRequest(r *http.Request) *Params {
params.FileId = props["file_id"]
params.Filename = query.Get("filename")
params.UploadId = props["upload_id"]
params.PluginId = props["plugin_id"]
if val, ok := props["plugin_id"]; ok {
params.PluginId = val
} else {
params.PluginId = query.Get("plugin_id")
}
params.CommandId = props["command_id"]
params.HookId = props["hook_id"]
params.ReportId = props["report_id"]
@@ -150,6 +162,14 @@ func ParamsFromRequest(r *http.Request) *Params {
params.RemoteId = props["remote_id"]
params.InvoiceId = props["invoice_id"]
params.OutgoingOAuthConnectionID = props["outgoing_oauth_connection_id"]
params.ExcludeOffline, _ = strconv.ParseBool(query.Get("exclude_offline"))
params.InChannel = query.Get("in_channel")
params.NotInChannel = query.Get("not_in_channel")
params.Topic = query.Get("topic")
params.CreatorId = query.Get("creator_id")
params.OnlyConfirmed, _ = strconv.ParseBool(query.Get("only_confirmed"))
params.OnlyPlugins, _ = strconv.ParseBool(query.Get("only_plugins"))
params.ExcludePlugins, _ = strconv.ParseBool(query.Get("exclude_plugins"))
params.ChannelBookmarkId = props["bookmark_id"]
params.Scope = query.Get("scope")

View File

@@ -1281,10 +1281,6 @@
"id": "api.command_remote.hint",
"translation": "[action]"
},
{
"id": "api.command_remote.incorrect_password.error",
"translation": "Could not decrypt invitation. Incorrect password or corrupt invitation: {{.Error}}"
},
{
"id": "api.command_remote.invitation.help",
"translation": "Invitation from secure connection"
@@ -2144,6 +2140,10 @@
"id": "api.getUsersForReporting.invalid_team_filter",
"translation": "Invalid team id provided."
},
{
"id": "api.get_site_url_error",
"translation": "Could not get the instance site url"
},
{
"id": "api.image.get.app_error",
"translation": "Requested image url cannot be parsed."
@@ -2726,14 +2726,34 @@
"id": "api.reaction.save_reaction.user_id.app_error",
"translation": "You cannot save reaction for the other user."
},
{
"id": "api.remote_cluster.accept_invitation_error",
"translation": "Could not accept the remote cluster invitation"
},
{
"id": "api.remote_cluster.base64_decode_error",
"translation": "Could not decode the base64 string"
},
{
"id": "api.remote_cluster.cluster_not_deleted",
"translation": "Remote cluster has not been deleted"
},
{
"id": "api.remote_cluster.delete.app_error",
"translation": "We encountered an error deleting the secure connection."
},
{
"id": "api.remote_cluster.encrypt_invite_error",
"translation": "Could not encrypt the remote cluster invite using the provided password"
},
{
"id": "api.remote_cluster.get.app_error",
"translation": "We encountered an error retrieving a secure connection."
},
{
"id": "api.remote_cluster.get.not_found",
"translation": "Remote Cluster not found"
},
{
"id": "api.remote_cluster.invalid_id.app_error",
"translation": "Invalid id."
@@ -2742,6 +2762,10 @@
"id": "api.remote_cluster.invalid_topic.app_error",
"translation": "Invalid topic."
},
{
"id": "api.remote_cluster.invite_decrypt_error",
"translation": "Could not decrypt the remote cluster invite using the provided password"
},
{
"id": "api.remote_cluster.save.app_error",
"translation": "We encountered an error saving the secure connection."

View File

@@ -12,7 +12,7 @@ import (
)
// AcceptInvitation is called when accepting an invitation to connect with a remote cluster.
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName, creatorId string, siteURL string) (*model.RemoteCluster, error) {
rc := &model.RemoteCluster{
RemoteId: invite.RemoteId,
Name: name,
@@ -29,7 +29,7 @@ func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name str
}
// confirm the invitation with the originating site
frame, err := makeConfirmFrame(rcSaved, teamId, siteURL)
frame, err := makeConfirmFrame(rcSaved, siteURL)
if err != nil {
return nil, err
}
@@ -60,7 +60,7 @@ func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name str
return rcSaved, nil
}
func makeConfirmFrame(rc *model.RemoteCluster, teamId string, siteURL string) (*model.RemoteClusterFrame, error) {
func makeConfirmFrame(rc *model.RemoteCluster, siteURL string) (*model.RemoteClusterFrame, error) {
confirm := model.RemoteClusterInvite{
RemoteId: rc.RemoteId,
SiteURL: siteURL,

View File

@@ -52,7 +52,7 @@ func (ms *mockServer) GetStore() store.Store {
remoteClusterStoreMock := &mocks.RemoteClusterStore{}
remoteClusterStoreMock.On("GetByTopic", "share").Return(ms.remotes, nil)
remoteClusterStoreMock.On("GetAll", anyQueryFilter).Return(ms.remotes, nil)
remoteClusterStoreMock.On("GetAll", 0, 999999, anyQueryFilter).Return(ms.remotes, nil)
remoteClusterStoreMock.On("SetLastPingAt", anyId).Return(nil)
userStoreMock := &mocks.UserStore{}

View File

@@ -37,7 +37,7 @@ func (rcs *Service) PingNow(rc *model.RemoteCluster) {
// pingAllNow emits a ping to all remotes immediately without waiting for next ping loop.
func (rcs *Service) pingAllNow(filter model.RemoteClusterQueryFilter) {
// get all remotes, including any previously offline.
remotes, err := rcs.server.GetStore().RemoteCluster().GetAll(filter)
remotes, err := rcs.server.GetStore().RemoteCluster().GetAll(0, 999999, filter)
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Ping all remote clusters failed (could not get list of remotes)", mlog.Err(err))
return

View File

@@ -41,7 +41,7 @@ func (rcs *Service) BroadcastMsg(ctx context.Context, msg model.RemoteClusterMsg
filter := model.RemoteClusterQueryFilter{
Topic: msg.Topic,
}
list, err := rcs.server.GetStore().RemoteCluster().GetAll(filter)
list, err := rcs.server.GetStore().RemoteCluster().GetAll(0, 999999, filter)
if err != nil {
return err
}

View File

@@ -68,7 +68,7 @@ type RemoteClusterServiceIFace interface {
SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f SendMsgResultFunc) error
SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp ReaderProvider, f SendFileResultFunc) error
SendProfileImage(ctx context.Context, userID string, rc *model.RemoteCluster, provider ProfileImageProvider, f SendProfileImageResultFunc) error
AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error)
AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, siteURL string) (*model.RemoteCluster, error)
ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) Response
ReceiveInviteConfirmation(invite model.RemoteClusterInvite) (*model.RemoteCluster, error)
PingNow(rc *model.RemoteCluster)

View File

@@ -258,7 +258,7 @@ func (scs *Service) processTask(task syncTask) error {
InChannel: task.channelID,
OnlyConfirmed: true,
}
remotes, err := scs.server.GetStore().RemoteCluster().GetAll(filter)
remotes, err := scs.server.GetStore().RemoteCluster().GetAll(0, 999999, filter)
if err != nil {
return err
}
@@ -270,7 +270,7 @@ func (scs *Service) processTask(task syncTask) error {
filter = model.RemoteClusterQueryFilter{
RequireOptions: model.BitflagOptionAutoInvited,
}
remotesAutoInvited, err := scs.server.GetStore().RemoteCluster().GetAll(filter)
remotesAutoInvited, err := scs.server.GetStore().RemoteCluster().GetAll(0, 999999, filter)
if err != nil {
return err
}

View File

@@ -568,6 +568,10 @@ func (c *Client4) exportRoute(name string) string {
return fmt.Sprintf(c.exportsRoute()+"/%v", name)
}
func (c *Client4) remoteClusterRoute() string {
return "/remotecluster"
}
func (c *Client4) sharedChannelsRoute() string {
return "/sharedchannels"
}
@@ -8698,6 +8702,154 @@ func (c *Client4) GetRemoteClusterInfo(ctx context.Context, remoteID string) (Re
return rci, BuildResponse(r), nil
}
func (c *Client4) GetRemoteClusters(ctx context.Context, page, perPage int, filter RemoteClusterQueryFilter) ([]*RemoteCluster, *Response, error) {
v := url.Values{}
if page != 0 {
v.Set("page", fmt.Sprintf("%d", page))
}
if perPage != 0 {
v.Set("per_page", fmt.Sprintf("%d", perPage))
}
if filter.ExcludeOffline {
v.Set("exclude_offline", "true")
}
if filter.InChannel != "" {
v.Set("in_channel", filter.InChannel)
}
if filter.NotInChannel != "" {
v.Set("not_in_channel", filter.NotInChannel)
}
if filter.Topic != "" {
v.Set("topic", filter.Topic)
}
if filter.CreatorId != "" {
v.Set("creator_id", filter.CreatorId)
}
if filter.OnlyConfirmed {
v.Set("only_confirmed", "true")
}
if filter.PluginID != "" {
v.Set("plugin_id", filter.PluginID)
}
if filter.OnlyPlugins {
v.Set("only_plugins", "true")
}
if filter.ExcludePlugins {
v.Set("exclude_plugins", "true")
}
url := c.remoteClusterRoute()
if len(v) > 0 {
url += "?" + v.Encode()
}
r, err := c.DoAPIGet(ctx, url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var rcs []*RemoteCluster
json.NewDecoder(r.Body).Decode(&rcs)
return rcs, BuildResponse(r), nil
}
func (c *Client4) CreateRemoteCluster(ctx context.Context, rcWithPassword *RemoteClusterWithPassword) (*RemoteClusterWithInvite, *Response, error) {
rcJSON, err := json.Marshal(rcWithPassword)
if err != nil {
return nil, nil, NewAppError("CreateRemoteCluster", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(ctx, c.remoteClusterRoute(), string(rcJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var rcWithInvite RemoteClusterWithInvite
if err := json.NewDecoder(r.Body).Decode(&rcWithInvite); err != nil {
return nil, nil, NewAppError("CreateRemoteCluster", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &rcWithInvite, BuildResponse(r), nil
}
func (c *Client4) RemoteClusterAcceptInvite(ctx context.Context, rcAcceptInvite *RemoteClusterAcceptInvite) (*RemoteCluster, *Response, error) {
rcAcceptInviteJSON, err := json.Marshal(rcAcceptInvite)
if err != nil {
return nil, nil, NewAppError("RemoteClusterAcceptInvite", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
url := fmt.Sprintf("%s/accept_invite", c.remoteClusterRoute())
r, err := c.DoAPIPost(ctx, url, string(rcAcceptInviteJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var rc RemoteCluster
if err := json.NewDecoder(r.Body).Decode(&rc); err != nil {
return nil, nil, NewAppError("RemoteClusterAcceptInvite", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &rc, BuildResponse(r), nil
}
func (c *Client4) GenerateRemoteClusterInvite(ctx context.Context, remoteClusterId, password string) (string, *Response, error) {
url := fmt.Sprintf("%s/%s/generate_invite", c.remoteClusterRoute(), remoteClusterId)
r, err := c.DoAPIPost(ctx, url, MapToJSON(map[string]string{"password": password}))
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
b, err := io.ReadAll(r.Body)
if err != nil {
return "", nil, NewAppError("GenerateRemoteClusterInvite", "api.read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return string(b), BuildResponse(r), nil
}
func (c *Client4) GetRemoteCluster(ctx context.Context, remoteClusterId string) (*RemoteCluster, *Response, error) {
r, err := c.DoAPIGet(ctx, fmt.Sprintf("%s/%s", c.remoteClusterRoute(), remoteClusterId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var rc *RemoteCluster
json.NewDecoder(r.Body).Decode(&rc)
return rc, BuildResponse(r), nil
}
func (c *Client4) PatchRemoteCluster(ctx context.Context, remoteClusterId string, patch *RemoteClusterPatch) (*RemoteCluster, *Response, error) {
patchJSON, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchRemoteCluster", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
url := fmt.Sprintf("%s/%s", c.remoteClusterRoute(), remoteClusterId)
r, err := c.DoAPIPatchBytes(ctx, url, patchJSON)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var rc RemoteCluster
if err := json.NewDecoder(r.Body).Decode(&rc); err != nil {
return nil, nil, NewAppError("PatchRemoteCluster", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &rc, BuildResponse(r), nil
}
func (c *Client4) DeleteRemoteCluster(ctx context.Context, remoteClusterId string) (*Response, error) {
r, err := c.DoAPIDelete(ctx, fmt.Sprintf("%s/%s", c.remoteClusterRoute(), remoteClusterId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) GetAncillaryPermissions(ctx context.Context, subsectionPermissions []string) ([]string, *Response, error) {
var returnedPermissions []string
url := fmt.Sprintf("%s/ancillary?subsection_permissions=%s", c.permissionsRoute(), strings.Join(subsectionPermissions, ","))

View File

@@ -126,6 +126,37 @@ func (rc *RemoteCluster) IsValid() *AppError {
return nil
}
func (rc *RemoteCluster) Sanitize() {
rc.Token = ""
rc.RemoteToken = ""
}
type RemoteClusterPatch struct {
DisplayName *string `json:"display_name"`
}
func (rcp *RemoteClusterPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"display_name": rcp.DisplayName,
}
}
func (rc *RemoteCluster) Patch(patch *RemoteClusterPatch) {
if patch.DisplayName != nil {
rc.DisplayName = *patch.DisplayName
}
}
type RemoteClusterWithPassword struct {
*RemoteCluster
Password string `json:"password"`
}
type RemoteClusterWithInvite struct {
RemoteCluster *RemoteCluster `json:"remote_cluster"`
Invite string `json:"invite"`
}
func newIDFromBytes(b []byte) string {
hash := md5.New()
_, _ = hash.Write(b)
@@ -393,6 +424,13 @@ func (rci *RemoteClusterInvite) Decrypt(encrypted []byte, password string) error
return json.Unmarshal(plain, &rci)
}
type RemoteClusterAcceptInvite struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Invite string `json:"invite"`
Password string `json:"password"`
}
// RemoteClusterQueryFilter provides filter criteria for RemoteClusterStore.GetAll
type RemoteClusterQueryFilter struct {
ExcludeOffline bool
@@ -403,5 +441,6 @@ type RemoteClusterQueryFilter struct {
OnlyConfirmed bool
PluginID string
OnlyPlugins bool
ExcludePlugins bool
RequireOptions Bitmask
}