коммит произвёл
GitHub
родитель
073bd3a6b7
Коммит
9408b98025
@@ -7,12 +7,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
@@ -275,13 +277,93 @@ func (a *App) CheckRolesExist(roleNames []string) *model.AppError {
|
||||
}
|
||||
|
||||
func (a *App) sendUpdatedRoleEvent(role *model.Role) *model.AppError {
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventRoleUpdated, "", "", "", nil, "")
|
||||
roleJSON, jsonErr := json.Marshal(role)
|
||||
if jsonErr != nil {
|
||||
return model.NewAppError("sendUpdatedRoleEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
}
|
||||
message.Add("role", string(roleJSON))
|
||||
a.Publish(message)
|
||||
|
||||
publishEvent := func(teamID, channelID string) {
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventRoleUpdated, teamID, channelID, "", nil, "")
|
||||
message.Add("role", string(roleJSON))
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
// Built-in system roles apply to all users; broadcast globally without a DB lookup.
|
||||
if role.BuiltIn {
|
||||
publishEvent("", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scheme-managed roles: use SchemeId to look up the owning scheme.
|
||||
if role.SchemeId == nil {
|
||||
// No owning scheme — treat as global (e.g. custom non-scheme role).
|
||||
publishEvent("", "")
|
||||
return nil
|
||||
}
|
||||
scheme, err := a.Srv().Store().Scheme().Get(*role.SchemeId)
|
||||
if err != nil {
|
||||
a.Log().Error("Failed to look up scheme for role event; skipping broadcast",
|
||||
mlog.String("role_id", role.Id),
|
||||
mlog.String("scheme_id", *role.SchemeId),
|
||||
mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
const pageSize = 1000
|
||||
const maxBroadcasts = 100000
|
||||
switch scheme.Scope {
|
||||
case model.SchemeScopeTeam:
|
||||
totalBroadcasts := 0
|
||||
offset := 0
|
||||
for {
|
||||
teams, storeErr := a.Srv().Store().Team().GetTeamsByScheme(scheme.Id, offset, pageSize)
|
||||
if storeErr != nil {
|
||||
return model.NewAppError("sendUpdatedRoleEvent", "app.role.send_updated_role_event.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
|
||||
}
|
||||
for _, team := range teams {
|
||||
publishEvent(team.Id, "")
|
||||
}
|
||||
totalBroadcasts += len(teams)
|
||||
if len(teams) < pageSize {
|
||||
break
|
||||
}
|
||||
if totalBroadcasts >= maxBroadcasts {
|
||||
a.Log().Error("sendUpdatedRoleEvent: hit broadcast limit for team scheme",
|
||||
mlog.String("scheme_id", scheme.Id),
|
||||
mlog.Int("totalBroadcasts", totalBroadcasts))
|
||||
break
|
||||
}
|
||||
offset += pageSize
|
||||
}
|
||||
case model.SchemeScopeChannel:
|
||||
totalBroadcasts := 0
|
||||
offset := 0
|
||||
for {
|
||||
channels, storeErr := a.Srv().Store().Channel().GetChannelsByScheme(scheme.Id, offset, pageSize)
|
||||
if storeErr != nil {
|
||||
return model.NewAppError("sendUpdatedRoleEvent", "app.role.send_updated_role_event.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
|
||||
}
|
||||
for _, channel := range channels {
|
||||
publishEvent("", channel.Id)
|
||||
}
|
||||
totalBroadcasts += len(channels)
|
||||
if len(channels) < pageSize {
|
||||
break
|
||||
}
|
||||
if totalBroadcasts >= maxBroadcasts {
|
||||
a.Log().Error("sendUpdatedRoleEvent: hit broadcast limit for channel scheme",
|
||||
mlog.String("scheme_id", scheme.Id),
|
||||
mlog.Int("totalBroadcasts", totalBroadcasts))
|
||||
break
|
||||
}
|
||||
offset += pageSize
|
||||
}
|
||||
case model.SchemeScopePlaybook, model.SchemeScopeRun:
|
||||
// Playbook/run schemes don't map to teams or channels; broadcast globally.
|
||||
publishEvent("", "")
|
||||
default:
|
||||
return model.NewAppError("sendUpdatedRoleEvent", "app.role.send_updated_role_event.unknown_scope", nil, fmt.Sprintf("unknown scheme scope: %s", scheme.Scope), http.StatusInternalServerError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
@@ -13,9 +14,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
type permissionInheritanceTestData struct {
|
||||
@@ -261,3 +264,225 @@ func testPermissionInheritance(t *testing.T, testCallback func(t *testing.T, th
|
||||
// test 24 combinations where the higher-scoped scheme is a TEAM scheme
|
||||
test(teamScheme.DefaultChannelGuestRole, teamScheme.DefaultChannelUserRole, teamScheme.DefaultChannelAdminRole)
|
||||
}
|
||||
|
||||
func TestSendUpdatedRoleEvent(t *testing.T) {
|
||||
t.Run("BuiltIn role broadcasts globally without a DB lookup", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
|
||||
role := &model.Role{Name: model.TeamAdminRoleId, BuiltIn: true}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
mockSchemeStore.AssertNotCalled(t, "Get", mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("Team scheme role calls GetTeamsByScheme and emits per-team events", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
roleName := model.NewId()
|
||||
scheme := &model.Scheme{Id: schemeID, Scope: model.SchemeScopeTeam}
|
||||
teams := []*model.Team{{Id: model.NewId()}, {Id: model.NewId()}}
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockTeamStore := mocks.TeamStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(scheme, nil)
|
||||
mockTeamStore.On("GetTeamsByScheme", schemeID, 0, 1000).Return(teams, nil)
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Team").Return(&mockTeamStore)
|
||||
|
||||
role := &model.Role{Name: roleName, BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
mockSchemeStore.AssertCalled(t, "Get", schemeID)
|
||||
mockTeamStore.AssertCalled(t, "GetTeamsByScheme", schemeID, 0, 1000)
|
||||
})
|
||||
|
||||
t.Run("Channel scheme role calls GetChannelsByScheme and emits per-channel events", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
roleName := model.NewId()
|
||||
scheme := &model.Scheme{Id: schemeID, Scope: model.SchemeScopeChannel}
|
||||
channels := model.ChannelList{{Id: model.NewId()}}
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockChannelStore := mocks.ChannelStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(scheme, nil)
|
||||
mockChannelStore.On("GetChannelsByScheme", schemeID, 0, 1000).Return(channels, nil)
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Channel").Return(&mockChannelStore)
|
||||
|
||||
role := &model.Role{Name: roleName, BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
mockSchemeStore.AssertCalled(t, "Get", schemeID)
|
||||
mockChannelStore.AssertCalled(t, "GetChannelsByScheme", schemeID, 0, 1000)
|
||||
})
|
||||
|
||||
t.Run("Role not in any scheme broadcasts globally", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockTeamStore := mocks.TeamStore{}
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Team").Return(&mockTeamStore)
|
||||
|
||||
role := &model.Role{Name: model.NewId(), BuiltIn: false, SchemeId: nil}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
mockSchemeStore.AssertNotCalled(t, "Get", mock.Anything)
|
||||
mockTeamStore.AssertNotCalled(t, "GetTeamsByScheme", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("Playbook scope falls back to global broadcast without querying teams or channels", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
roleName := model.NewId()
|
||||
scheme := &model.Scheme{Id: schemeID, Scope: model.SchemeScopePlaybook}
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockTeamStore := mocks.TeamStore{}
|
||||
mockChannelStore := mocks.ChannelStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(scheme, nil)
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Team").Return(&mockTeamStore)
|
||||
mockStore.On("Channel").Return(&mockChannelStore)
|
||||
|
||||
role := &model.Role{Name: roleName, BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
mockTeamStore.AssertNotCalled(t, "GetTeamsByScheme", mock.Anything, mock.Anything, mock.Anything)
|
||||
mockChannelStore.AssertNotCalled(t, "GetChannelsByScheme", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("Scheme store error is logged and skips broadcast", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
roleName := model.NewId()
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(nil, errors.New("db error"))
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
|
||||
role := &model.Role{Name: roleName, BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("GetTeamsByScheme store error propagates as AppError", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
roleName := model.NewId()
|
||||
scheme := &model.Scheme{Id: schemeID, Scope: model.SchemeScopeTeam}
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockTeamStore := mocks.TeamStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(scheme, nil)
|
||||
mockTeamStore.On("GetTeamsByScheme", schemeID, 0, 1000).Return(nil, errors.New("db error"))
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Team").Return(&mockTeamStore)
|
||||
|
||||
role := &model.Role{Name: roleName, BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.NotNil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("Team scheme paginates across multiple pages", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
scheme := &model.Scheme{Id: schemeID, Scope: model.SchemeScopeTeam}
|
||||
|
||||
// Build a full first page (1000 teams) and a partial second page (2 teams).
|
||||
page1 := make([]*model.Team, 1000)
|
||||
for i := range page1 {
|
||||
page1[i] = &model.Team{Id: model.NewId()}
|
||||
}
|
||||
page2 := []*model.Team{{Id: model.NewId()}, {Id: model.NewId()}}
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockTeamStore := mocks.TeamStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(scheme, nil)
|
||||
mockTeamStore.On("GetTeamsByScheme", schemeID, 0, 1000).Return(page1, nil)
|
||||
mockTeamStore.On("GetTeamsByScheme", schemeID, 1000, 1000).Return(page2, nil)
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Team").Return(&mockTeamStore)
|
||||
|
||||
role := &model.Role{Name: model.NewId(), BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
mockTeamStore.AssertCalled(t, "GetTeamsByScheme", schemeID, 0, 1000)
|
||||
mockTeamStore.AssertCalled(t, "GetTeamsByScheme", schemeID, 1000, 1000)
|
||||
})
|
||||
|
||||
t.Run("Channel scheme paginates across multiple pages", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
scheme := &model.Scheme{Id: schemeID, Scope: model.SchemeScopeChannel}
|
||||
|
||||
page1 := make(model.ChannelList, 1000)
|
||||
for i := range page1 {
|
||||
page1[i] = &model.Channel{Id: model.NewId()}
|
||||
}
|
||||
page2 := model.ChannelList{{Id: model.NewId()}, {Id: model.NewId()}, {Id: model.NewId()}}
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockChannelStore := mocks.ChannelStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(scheme, nil)
|
||||
mockChannelStore.On("GetChannelsByScheme", schemeID, 0, 1000).Return(page1, nil)
|
||||
mockChannelStore.On("GetChannelsByScheme", schemeID, 1000, 1000).Return(page2, nil)
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Channel").Return(&mockChannelStore)
|
||||
|
||||
role := &model.Role{Name: model.NewId(), BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.Nil(t, appErr)
|
||||
mockChannelStore.AssertCalled(t, "GetChannelsByScheme", schemeID, 0, 1000)
|
||||
mockChannelStore.AssertCalled(t, "GetChannelsByScheme", schemeID, 1000, 1000)
|
||||
})
|
||||
|
||||
t.Run("GetChannelsByScheme store error propagates as AppError", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
schemeID := model.NewId()
|
||||
roleName := model.NewId()
|
||||
scheme := &model.Scheme{Id: schemeID, Scope: model.SchemeScopeChannel}
|
||||
|
||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||
mockSchemeStore := mocks.SchemeStore{}
|
||||
mockChannelStore := mocks.ChannelStore{}
|
||||
mockSchemeStore.On("Get", schemeID).Return(scheme, nil)
|
||||
mockChannelStore.On("GetChannelsByScheme", schemeID, 0, 1000).Return(nil, errors.New("db error"))
|
||||
mockStore.On("Scheme").Return(&mockSchemeStore)
|
||||
mockStore.On("Channel").Return(&mockChannelStore)
|
||||
|
||||
role := &model.Role{Name: roleName, BuiltIn: false, SchemeId: &schemeID}
|
||||
appErr := th.App.sendUpdatedRoleEvent(role)
|
||||
require.NotNil(t, appErr)
|
||||
})
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user