mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 13:08:56 -04:00
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (shard 0) (push) Blocked by required conditions
Server CI / Postgres (shard 1) (push) Blocked by required conditions
Server CI / Postgres (shard 2) (push) Blocked by required conditions
Server CI / Postgres (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres Test Results (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Tools CI / check-style (mattermost-govet) (push) Waiting to run
Tools CI / Test (mattermost-govet) (push) Waiting to run
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* MM-68158: Fix shared channel remote display and add WebSocket notification Fix getSharedChannelRemotes API handler passing ChannelId instead of RemoteId to GetRemoteCluster, which always failed the lookup. Add RemoteId to SharedChannelRemoteStatus model and store query. Add shared_channel_remote_updated WebSocket event published from the onInvite callback so the UI refreshes its cached remote names when the async invite completes, instead of showing the generic "Shared with trusted organizations" fallback. * Improved unit tests per review comments
352 lines
11 KiB
Go
352 lines
11 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
func (api *API) InitSharedChannels() {
|
|
api.BaseRoutes.SharedChannels.Handle("/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getSharedChannels)).Methods(http.MethodGet)
|
|
api.BaseRoutes.SharedChannels.Handle("/remote_info/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteClusterInfo)).Methods(http.MethodGet)
|
|
api.BaseRoutes.SharedChannels.Handle("/{channel_id:[A-Za-z0-9]+}/remotes", api.APISessionRequired(getSharedChannelRemotes)).Methods(http.MethodGet)
|
|
api.BaseRoutes.SharedChannels.Handle("/users/{user_id:[A-Za-z0-9]+}/can_dm/{other_user_id:[A-Za-z0-9]+}", api.APISessionRequired(canUserDirectMessage)).Methods(http.MethodGet)
|
|
|
|
api.BaseRoutes.SharedChannelRemotes.Handle("", api.APISessionRequired(getSharedChannelRemotesByRemoteCluster)).Methods(http.MethodGet)
|
|
api.BaseRoutes.ChannelForRemote.Handle("/invite", api.APISessionRequired(inviteRemoteClusterToChannel)).Methods(http.MethodPost)
|
|
api.BaseRoutes.ChannelForRemote.Handle("/uninvite", api.APISessionRequired(uninviteRemoteClusterToChannel)).Methods(http.MethodPost)
|
|
}
|
|
|
|
func getSharedChannels(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireTeamId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// make sure remote cluster service is enabled.
|
|
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
// make sure user has access to the team.
|
|
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
|
c.SetPermissionError(model.PermissionViewTeam)
|
|
return
|
|
}
|
|
|
|
opts := model.SharedChannelFilterOpts{
|
|
TeamId: c.Params.TeamId,
|
|
}
|
|
|
|
// only return channels the user is a member of, unless they are a shared channels manager.
|
|
if !c.App.HasPermissionTo(c.AppContext.Session().UserId, model.PermissionManageSharedChannels) {
|
|
opts.MemberId = c.AppContext.Session().UserId
|
|
}
|
|
|
|
channels, appErr := c.App.GetSharedChannels(c.Params.Page, c.Params.PerPage, opts)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
b, err := json.Marshal(channels)
|
|
if err != nil {
|
|
c.SetJSONEncodingError(err)
|
|
return
|
|
}
|
|
|
|
if _, err := w.Write(b); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getRemoteClusterInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
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
|
|
}
|
|
|
|
// GetRemoteClusterForUser will only return a remote if the user is a member of at
|
|
// least one channel shared by the remote. All other cases return error.
|
|
rc, appErr := c.App.GetRemoteClusterForUser(c.Params.RemoteId, c.AppContext.Session().UserId, c.Params.IncludeDeleted)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
remoteInfo := rc.ToRemoteClusterInfo()
|
|
|
|
b, err := json.Marshal(remoteInfo)
|
|
if err != nil {
|
|
c.SetJSONEncodingError(err)
|
|
return
|
|
}
|
|
if _, err := w.Write(b); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getSharedChannelRemotesByRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireRemoteId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
c.RequirePermissionToManageSecureConnectionsOrSharedChannels()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// make sure remote cluster service is enabled.
|
|
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId, true); appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
filter := model.SharedChannelRemoteFilterOpts{
|
|
RemoteId: c.Params.RemoteId,
|
|
IncludeUnconfirmed: c.Params.IncludeUnconfirmed,
|
|
ExcludeConfirmed: c.Params.ExcludeConfirmed,
|
|
ExcludeHome: c.Params.ExcludeHome,
|
|
ExcludeRemote: c.Params.ExcludeRemote,
|
|
IncludeDeleted: c.Params.IncludeDeleted,
|
|
}
|
|
sharedChannelRemotes, err := c.App.GetSharedChannelRemotes(c.Params.Page, c.Params.PerPage, filter)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("getSharedChannelRemotesByRemoteCluster", "api.shared_channel.get_shared_channel_remotes_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(sharedChannelRemotes); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func inviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireRemoteId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
c.RequireChannelId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
c.RequirePermissionToManageSharedChannels()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// make sure remote cluster service is enabled.
|
|
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId, false); appErr != nil {
|
|
c.SetInvalidRemoteIdError(c.Params.RemoteId)
|
|
return
|
|
}
|
|
|
|
if _, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId); appErr != nil {
|
|
c.SetInvalidURLParam("channel_id")
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventInviteRemoteClusterToChannel, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "remote_id", c.Params.RemoteId)
|
|
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
|
model.AddEventParameterToAuditRec(auditRec, "user_id", c.AppContext.Session().UserId)
|
|
|
|
if err := c.App.InviteRemoteToChannel(c.Params.ChannelId, c.Params.RemoteId, c.AppContext.Session().UserId, true); err != nil {
|
|
if appErr, ok := err.(*model.AppError); ok {
|
|
c.Err = appErr
|
|
} else {
|
|
c.Err = model.NewAppError("inviteRemoteClusterToChannel", "api.shared_channel.invite_remote_to_channel_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func uninviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireRemoteId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
c.RequireChannelId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
c.RequirePermissionToManageSharedChannels()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// make sure remote cluster service is enabled.
|
|
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId, false); appErr != nil {
|
|
c.SetInvalidRemoteIdError(c.Params.RemoteId)
|
|
return
|
|
}
|
|
|
|
if _, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId); appErr != nil {
|
|
c.SetInvalidURLParam("channel_id")
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventUninviteRemoteClusterToChannel, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "remote_id", c.Params.RemoteId)
|
|
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
|
|
|
hasRemote, err := c.App.HasRemote(c.Params.ChannelId, c.Params.RemoteId)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uninviteRemoteClusterToChannel", "api.shared_channel.has_remote_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
// if the channel is not shared with the remote, we return early
|
|
if !hasRemote {
|
|
auditRec.Success()
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
if err := c.App.UninviteRemoteFromChannel(c.Params.ChannelId, c.Params.RemoteId); err != nil {
|
|
c.Err = model.NewAppError("uninviteRemoteClusterToChannel", "api.shared_channel.uninvite_remote_to_channel_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
// getSharedChannelRemotes returns info about remote clusters for a shared channel
|
|
func getSharedChannelRemotes(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireChannelId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// make sure remote cluster service is enabled.
|
|
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
|
c.SetPermissionError(model.PermissionReadChannel)
|
|
return
|
|
}
|
|
|
|
// Get the remotes status
|
|
remoteStatuses, err := c.App.GetSharedChannelRemotesStatus(c.Params.ChannelId)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("getSharedChannelRemotes", "api.command_share.fetch_remote_status.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
// For each remote status, get the RemoteClusterInfo
|
|
remoteInfos := make([]*model.RemoteClusterInfo, 0, len(remoteStatuses))
|
|
for _, status := range remoteStatuses {
|
|
// Use GetRemoteCluster to get the full remote cluster
|
|
remoteCluster, appErr := c.App.GetRemoteCluster(status.RemoteId, false)
|
|
if appErr == nil && remoteCluster != nil {
|
|
info := remoteCluster.ToRemoteClusterInfo()
|
|
remoteInfos = append(remoteInfos, &info)
|
|
} else {
|
|
// If we can't find the detailed info, create a basic RemoteClusterInfo from the status
|
|
remoteInfos = append(remoteInfos, &model.RemoteClusterInfo{
|
|
Name: status.DisplayName,
|
|
DisplayName: status.DisplayName,
|
|
LastPingAt: status.LastPingAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(remoteInfos); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func canUserDirectMessage(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireUserId().RequireOtherUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// Check if the user can see the other user at all
|
|
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.Params.UserId, c.Params.OtherUserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if !canSee {
|
|
result := map[string]bool{"can_dm": false}
|
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
|
c.Logger.Warn("Error encoding JSON response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
canDM := true
|
|
|
|
// Get shared channel sync service for remote user checks
|
|
scs := c.App.Srv().GetSharedChannelSyncService()
|
|
if scs != nil {
|
|
otherUser, otherErr := c.App.GetUser(c.Params.OtherUserId)
|
|
if otherErr != nil {
|
|
canDM = false
|
|
} else {
|
|
originalRemoteId := otherUser.GetOriginalRemoteID()
|
|
|
|
// Check if the other user is from a remote cluster
|
|
if otherUser.IsRemote() {
|
|
// If original remote ID is unknown, fall back to current RemoteId as best guess
|
|
if originalRemoteId == model.UserOriginalRemoteIdUnknown {
|
|
originalRemoteId = otherUser.GetRemoteID()
|
|
}
|
|
|
|
// For DMs, we require a direct connection to the ORIGINAL remote cluster
|
|
isDirectlyConnected := scs.IsRemoteClusterDirectlyConnected(originalRemoteId)
|
|
|
|
if !isDirectlyConnected {
|
|
canDM = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result := map[string]bool{"can_dm": canDM}
|
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
|
c.Logger.Warn("Error encoding JSON response", mlog.Err(err))
|
|
}
|
|
}
|