Files
mostlymatter/server/channels/api4/remote_cluster.go
Miguel de la Cruz 3466e6f5d8 Avoid using token from sanitized remote cluster (#28335)
After creating a remote cluster, we were using its token field to
generate the remote invite. As we're going to return that remote
cluster in the response, we sanitize the entity, emptying the token
field before generating the invite, and causing it not to contain the
token itself.

This changes store the token in an independent variable to avoid
mutating it as part of the sanitization.

Co-authored-by: Mattermost Build <build@mattermost.com>
2024-10-08 16:49:33 +02:00

676 строки
20 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/audit"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/platform/services/remotecluster"
)
func (api *API) InitRemoteCluster() {
api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData, handlerParamFileAPI)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage, handlerParamFileAPI)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("", api.APISessionRequired(getRemoteClusters)).Methods(http.MethodGet)
api.BaseRoutes.RemoteCluster.Handle("", api.APISessionRequired(createRemoteCluster)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("/accept_invite", api.APISessionRequired(remoteClusterAcceptInvite)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}/generate_invite", api.APISessionRequired(generateRemoteClusterInvite)).Methods(http.MethodPost)
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteCluster)).Methods(http.MethodGet)
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(patchRemoteCluster)).Methods(http.MethodPatch)
api.BaseRoutes.RemoteCluster.Handle("/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteRemoteCluster)).Methods(http.MethodDelete)
}
func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterPing", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId, false)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
var ping model.RemoteClusterPing
if err := json.Unmarshal(frame.Msg.Payload, &ping); err != nil {
c.SetInvalidParamWithErr("msg.payload", err)
return
}
ping.RecvAt = model.GetMillis()
if metrics := c.App.Metrics(); metrics != nil {
metrics.IncrementRemoteClusterMsgReceivedCounter(rc.RemoteId)
}
err := json.NewEncoder(w).Encode(ping)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func remoteClusterAcceptMessage(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
service, appErr := c.App.GetRemoteClusterService()
if appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
appErr = frame.IsValid()
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptMessage", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId, false)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
// pass message to Remote Cluster Service and write response
resp := service.ReceiveIncomingMsg(rc, frame.Msg)
b, err := json.Marshal(resp)
if err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func remoteClusterConfirmInvite(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
rcs, appErr := c.App.GetRemoteClusterService()
if appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if jsonErr := json.NewDecoder(r.Body).Decode(&frame); jsonErr != nil {
c.Err = model.NewAppError("remoteClusterConfirmInvite", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptInvite", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, err := c.App.GetRemoteCluster(frame.RemoteId, false)
if err != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
// check if the invitation has expired
if time.Since(model.GetTimeForMillis(rc.CreateAt)) > remotecluster.InviteExpiresAfter {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.context.invitation_expired.error", nil, "", http.StatusBadRequest)
return
}
var confirm model.RemoteClusterInvite
if jsonErr := json.Unmarshal(frame.Msg.Payload, &confirm); jsonErr != nil {
c.SetInvalidParam("msg.payload")
return
}
if _, rcsErr := rcs.ReceiveInviteConfirmation(confirm); rcsErr != nil {
c.Err = model.NewAppError("remoteClusterConfirmInvite", "api.command_remote.confirm_invitation.error",
map[string]any{"Error": rcsErr.Error()}, "", http.StatusInternalServerError)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func uploadRemoteData(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadRemoteData", "api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
c.RequireUploadId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("uploadRemoteData", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
c.AppContext = c.AppContext.With(app.RequestContextWithMaster)
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.RemoteId != c.GetRemoteID(r) {
c.Err = model.NewAppError("uploadRemoteData", "api.context.remote_id_mismatch.app_error",
nil, "", http.StatusUnauthorized)
return
}
info, err := doUploadData(c, us, r)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if info == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireUserId()
if c.Err != nil {
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) == 0 {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("remoteUploadProfileImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if imageArray[0] != nil {
audit.AddEventParameter(auditRec, "filename", imageArray[0].Filename)
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil || !user.IsRemote() {
c.SetInvalidURLParam("user_id")
return
}
// ensure the user being modified belongs to the remote requesting the change.
requesterRemoteID := c.GetRemoteID(r)
if user.GetRemoteID() != requesterRemoteID {
c.Err = model.NewAppError("remoteSetProfileImage", "api.context.remote_id_mismatch.app_error",
nil, "", http.StatusUnauthorized)
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
imageData := imageArray[0]
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
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,
IncludeDeleted: c.Params.IncludeDeleted,
}
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
}
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
}
token := model.NewId()
rc := &model.RemoteCluster{
Name: rcWithTeamAndPassword.Name,
DisplayName: rcWithTeamAndPassword.DisplayName,
SiteURL: model.SiteURLPending + model.NewId(),
DefaultTeamId: rcWithTeamAndPassword.DefaultTeamId,
Token: token,
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()
password := rcWithTeamAndPassword.Password
if password == "" {
password = utils.SecureRandString(16)
}
inviteCode, iErr := c.App.CreateRemoteClusterInvite(rcSaved.RemoteId, url, token, password)
if iErr != nil {
c.Err = iErr
return
}
auditRec.Success()
auditRec.AddEventResultState(rcSaved)
auditRec.AddEventObjectType("remotecluster")
resp := model.RemoteClusterWithInvite{RemoteCluster: rcSaved, Invite: inviteCode}
if rcWithTeamAndPassword.Password == "" {
resp.Password = password
}
b, err := json.Marshal(resp)
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, false)
if appErr != nil {
c.Err = appErr
return
}
if rc.IsConfirmed() {
c.Err = model.NewAppError("generateRemoteClusterInvite", "api.remote_cluster.generate_invite_cluster_is_confirmed", nil, "", http.StatusBadRequest)
return
}
inviteCode, invErr := c.App.CreateRemoteClusterInvite(rc.RemoteId, url, rc.Token, password)
if invErr != nil {
c.Err = invErr
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(inviteCode); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
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, true)
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, false)
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, false)
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()
ReturnStatusOK(w)
}