snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
28
server/channels/app/teams/errors.go
Normal file
28
server/channels/app/teams/errors.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package teams
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
AcceptedDomainError = errors.New("the user cannot be added as the domain associated with the account is not permitted")
|
||||
MemberCountError = errors.New("unable to count the team members")
|
||||
MaxMemberCountError = errors.New("reached to the maximum number of allowed accounts")
|
||||
)
|
||||
|
||||
type DomainError struct {
|
||||
Domain string
|
||||
}
|
||||
|
||||
func (DomainError) Error() string {
|
||||
return "restricting team to the domain, it is not allowed by the system config"
|
||||
}
|
||||
|
||||
type NameOccupiedError struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (NameOccupiedError) Error() string {
|
||||
return "team name is already occupied"
|
||||
}
|
||||
132
server/channels/app/teams/helper_test.go
Normal file
132
server/channels/app/teams/helper_test.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package teams
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
service *TeamService
|
||||
configStore *config.Store
|
||||
dbStore store.Store
|
||||
workspace string
|
||||
|
||||
Context *request.Context
|
||||
LogBuffer *bytes.Buffer
|
||||
TB testing.TB
|
||||
}
|
||||
|
||||
type mockWebHub struct{}
|
||||
|
||||
func (mockWebHub) Publish(*model.WebSocketEvent) {}
|
||||
|
||||
func Setup(tb testing.TB) *TestHelper {
|
||||
if testing.Short() {
|
||||
tb.SkipNow()
|
||||
}
|
||||
dbStore := mainHelper.GetStore()
|
||||
dbStore.DropAllTables()
|
||||
dbStore.MarkSystemRanUnitTests()
|
||||
mainHelper.PreloadMigrations()
|
||||
|
||||
return setupTestHelper(dbStore, false, tb)
|
||||
}
|
||||
|
||||
func setupTestHelper(s store.Store, includeCacheLayer bool, tb testing.TB) *TestHelper {
|
||||
tempWorkspace, err := os.MkdirTemp("", "teamservicetest")
|
||||
require.NoError(tb, err)
|
||||
|
||||
configStore := config.NewTestMemoryStore()
|
||||
|
||||
config := configStore.Get()
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*config.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
*config.LogSettings.EnableSentry = false // disable error reporting during tests
|
||||
*config.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*config.AnnouncementSettings.UserNoticesEnabled = false
|
||||
*config.TeamSettings.MaxUsersPerTeam = 50
|
||||
*config.RateLimitSettings.Enable = false
|
||||
*config.TeamSettings.EnableOpenServer = true
|
||||
_, _, err = configStore.Set(config)
|
||||
require.NoError(tb, err)
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
tb.Cleanup(func() {
|
||||
err := configStore.Close()
|
||||
require.NoError(tb, err)
|
||||
|
||||
s.Close()
|
||||
|
||||
if tempWorkspace != "" {
|
||||
os.RemoveAll(tempWorkspace)
|
||||
}
|
||||
})
|
||||
|
||||
return &TestHelper{
|
||||
service: &TeamService{
|
||||
store: s.Team(),
|
||||
channelStore: s.Channel(),
|
||||
groupStore: s.Group(),
|
||||
config: configStore.Get,
|
||||
license: func() *model.License {
|
||||
return model.NewTestLicense()
|
||||
},
|
||||
wh: &mockWebHub{},
|
||||
},
|
||||
Context: request.EmptyContext(mlog.CreateConsoleTestLogger(tb)),
|
||||
configStore: configStore,
|
||||
dbStore: s,
|
||||
LogBuffer: buffer,
|
||||
workspace: tempWorkspace,
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) UpdateConfig(f func(*model.Config)) {
|
||||
if th.configStore.IsReadOnly() {
|
||||
return
|
||||
}
|
||||
old := th.configStore.Get()
|
||||
updated := old.Clone()
|
||||
f(updated)
|
||||
_, _, err := th.configStore.Set(updated)
|
||||
require.NoError(th.TB, err)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateUser(u *model.User) *model.User {
|
||||
u.EmailVerified = true
|
||||
user, err := th.dbStore.User().Save(th.Context, u)
|
||||
require.NoError(th.TB, err)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func (th *TestHelper) DeleteUser(u *model.User) {
|
||||
err := th.dbStore.User().PermanentDelete(th.Context, u.Id)
|
||||
require.NoError(th.TB, err)
|
||||
}
|
||||
|
||||
func (th *TestHelper) DeleteTeam(t *model.Team) {
|
||||
err := th.dbStore.Channel().PermanentDeleteByTeam(t.Id)
|
||||
require.NoError(th.TB, err)
|
||||
|
||||
err = th.dbStore.Team().RemoveAllMembersByTeam(t.Id)
|
||||
require.NoError(th.TB, err)
|
||||
|
||||
err = th.dbStore.Team().PermanentDelete(t.Id)
|
||||
require.NoError(th.TB, err)
|
||||
}
|
||||
24
server/channels/app/teams/main_test.go
Normal file
24
server/channels/app/teams/main_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package teams
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var options = testlib.HelperOptions{
|
||||
EnableStore: true,
|
||||
EnableResources: true,
|
||||
}
|
||||
|
||||
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
69
server/channels/app/teams/service.go
Normal file
69
server/channels/app/teams/service.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package teams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
)
|
||||
|
||||
type TeamService struct {
|
||||
store store.TeamStore
|
||||
groupStore store.GroupStore
|
||||
channelStore store.ChannelStore // TODO: replace this with ChannelService in the future
|
||||
users Users
|
||||
wh WebHub
|
||||
config func() *model.Config
|
||||
license func() *model.License
|
||||
}
|
||||
|
||||
// ServiceConfig is used to initialize the TeamService.
|
||||
type ServiceConfig struct {
|
||||
// Mandatory fields
|
||||
TeamStore store.TeamStore
|
||||
GroupStore store.GroupStore
|
||||
ChannelStore store.ChannelStore
|
||||
Users Users
|
||||
WebHub WebHub
|
||||
ConfigFn func() *model.Config
|
||||
LicenseFn func() *model.License
|
||||
}
|
||||
|
||||
// Users is a subset of UserService interface
|
||||
type Users interface {
|
||||
GetUser(rctx request.CTX, userID string) (*model.User, error)
|
||||
}
|
||||
|
||||
// WebHub is used to publish events, the name should be given appropriately
|
||||
// while developing the websocket or clustering service
|
||||
type WebHub interface {
|
||||
Publish(message *model.WebSocketEvent)
|
||||
}
|
||||
|
||||
func New(c ServiceConfig) (*TeamService, error) {
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TeamService{
|
||||
store: c.TeamStore,
|
||||
groupStore: c.GroupStore,
|
||||
channelStore: c.ChannelStore,
|
||||
users: c.Users,
|
||||
config: c.ConfigFn,
|
||||
license: c.LicenseFn,
|
||||
wh: c.WebHub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) validate() error {
|
||||
if c.ConfigFn == nil || c.TeamStore == nil || c.LicenseFn == nil || c.Users == nil || c.ChannelStore == nil || c.GroupStore == nil || c.WebHub == nil {
|
||||
return errors.New("required parameters are not provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
279
server/channels/app/teams/teams.go
Normal file
279
server/channels/app/teams/teams.go
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package teams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
)
|
||||
|
||||
func (ts *TeamService) CreateTeam(rctx request.CTX, team *model.Team) (*model.Team, error) {
|
||||
team.InviteId = ""
|
||||
rteam, err := ts.store.Save(team)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := ts.createDefaultChannels(rctx, rteam.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rteam, nil
|
||||
}
|
||||
|
||||
func (ts *TeamService) GetTeam(teamID string) (*model.Team, error) {
|
||||
team, err := ts.store.Get(teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (ts *TeamService) GetTeams(teamIDs []string) ([]*model.Team, error) {
|
||||
teams, err := ts.store.GetMany(teamIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return teams, nil
|
||||
}
|
||||
|
||||
// CreateDefaultChannels creates channels in the given team for each channel returned by (*App).DefaultChannelNames.
|
||||
func (ts *TeamService) createDefaultChannels(rctx request.CTX, teamID string) ([]*model.Channel, error) {
|
||||
displayNames := map[string]string{
|
||||
"town-square": i18n.T("api.channel.create_default_channels.town_square"),
|
||||
"off-topic": i18n.T("api.channel.create_default_channels.off_topic"),
|
||||
}
|
||||
channels := []*model.Channel{}
|
||||
defaultChannelNames := ts.DefaultChannelNames()
|
||||
for _, name := range defaultChannelNames {
|
||||
var displayName string
|
||||
if displayNameValue, ok := displayNames[name]; ok {
|
||||
displayName = i18n.TDefault(displayNameValue, name)
|
||||
} else {
|
||||
// If the default channel is experimental (from config.json)
|
||||
// we don't have to translate
|
||||
displayName = name
|
||||
}
|
||||
channel := &model.Channel{DisplayName: displayName, Name: name, Type: model.ChannelTypeOpen, TeamId: teamID}
|
||||
// We should use the channel service here (coming soon). Ideally, we should just emit an event
|
||||
// and let the subscribers do the job, in this case it would be the channels service.
|
||||
// Currently we are adding services to the server and because of that we are using
|
||||
// the channel store here. This should be replaced in the future.
|
||||
if _, err := ts.channelStore.Save(rctx, channel, *ts.config().TeamSettings.MaxChannelsPerTeam); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
type UpdateOptions struct {
|
||||
Sanitized bool
|
||||
Imported bool
|
||||
}
|
||||
|
||||
func (ts *TeamService) UpdateTeam(team *model.Team, opts UpdateOptions) (*model.Team, error) {
|
||||
oldTeam := team
|
||||
var err error
|
||||
|
||||
if !opts.Imported {
|
||||
oldTeam, err = ts.store.Get(team.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = ts.checkValidDomains(team); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Sanitized {
|
||||
if team.Name != "" && team.Name != oldTeam.Name && team.Name != "-" {
|
||||
_, nErr := ts.store.GetByName(team.Name)
|
||||
if nErr == nil {
|
||||
return oldTeam, &NameOccupiedError{Name: team.Name}
|
||||
}
|
||||
var nfErr *store.ErrNotFound
|
||||
if !errors.As(nErr, &nfErr) {
|
||||
return oldTeam, nErr
|
||||
}
|
||||
oldTeam.Name = team.Name
|
||||
}
|
||||
oldTeam.DisplayName = team.DisplayName
|
||||
oldTeam.Description = team.Description
|
||||
oldTeam.AllowOpenInvite = team.AllowOpenInvite
|
||||
oldTeam.CompanyName = team.CompanyName
|
||||
oldTeam.AllowedDomains = team.AllowedDomains
|
||||
oldTeam.LastTeamIconUpdate = team.LastTeamIconUpdate
|
||||
oldTeam.GroupConstrained = team.GroupConstrained
|
||||
}
|
||||
|
||||
oldTeam, err = ts.store.Update(oldTeam)
|
||||
if err != nil {
|
||||
return team, err
|
||||
}
|
||||
|
||||
return oldTeam, nil
|
||||
}
|
||||
|
||||
func (ts *TeamService) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, error) {
|
||||
team, err := ts.store.Get(teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
team.Patch(patch)
|
||||
if patch.AllowOpenInvite != nil && !*patch.AllowOpenInvite {
|
||||
team.InviteId = model.NewId()
|
||||
}
|
||||
|
||||
if err = ts.checkValidDomains(team); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
team, err = ts.store.Update(team)
|
||||
if err != nil {
|
||||
return team, err
|
||||
}
|
||||
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func applyPreSaveHooks(hooks []func(*model.TeamMember) (*model.TeamMember, error), tm *model.TeamMember) (*model.TeamMember, error) {
|
||||
for _, hook := range hooks {
|
||||
var err error
|
||||
tm, err = hook(tm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tm == nil {
|
||||
return nil, fmt.Errorf("preSaveHook returned nil TeamMember without error")
|
||||
}
|
||||
}
|
||||
return tm, nil
|
||||
}
|
||||
|
||||
// JoinUserToTeam adds a user to the team and it returns three values:
|
||||
// 1. a pointer to the team member, if successful
|
||||
// 2. a boolean: true if the user has a non-deleted team member for that team already, otherwise false.
|
||||
// 3. a pointer to an AppError if something went wrong.
|
||||
//
|
||||
// An optional preSaveHook can be provided to inspect/modify the TeamMember before it is persisted.
|
||||
// If the hook returns an error, the addition is rejected.
|
||||
func (ts *TeamService) JoinUserToTeam(rctx request.CTX, team *model.Team, user *model.User, preSaveHooks ...func(*model.TeamMember) (*model.TeamMember, error)) (*model.TeamMember, bool, error) {
|
||||
if !ts.IsTeamEmailAllowed(user, team) {
|
||||
return nil, false, AcceptedDomainError
|
||||
}
|
||||
|
||||
tm := &model.TeamMember{
|
||||
TeamId: team.Id,
|
||||
UserId: user.Id,
|
||||
SchemeGuest: user.IsGuest(),
|
||||
SchemeUser: !user.IsGuest(),
|
||||
CreateAt: model.GetMillis(),
|
||||
}
|
||||
|
||||
if !user.IsGuest() {
|
||||
userShouldBeAdmin, err := ts.userIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
tm.SchemeAdmin = userShouldBeAdmin
|
||||
}
|
||||
|
||||
if team.Email == user.Email && !user.IsGuest() {
|
||||
tm.SchemeAdmin = true
|
||||
}
|
||||
|
||||
rtm, err := ts.store.GetMember(rctx, team.Id, user.Id)
|
||||
if err != nil {
|
||||
// Membership appears to be missing. Lets try to add.
|
||||
tm, err = applyPreSaveHooks(preSaveHooks, tm)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
tmr, nErr := ts.store.SaveMember(rctx, tm, *ts.config().TeamSettings.MaxUsersPerTeam)
|
||||
if nErr != nil {
|
||||
return nil, false, nErr
|
||||
}
|
||||
return tmr, false, nil
|
||||
}
|
||||
|
||||
// Membership already exists. Check if deleted and update, otherwise do nothing
|
||||
// Do nothing if already added
|
||||
if rtm.DeleteAt == 0 {
|
||||
return rtm, true, nil
|
||||
}
|
||||
|
||||
membersCount, err := ts.store.GetActiveMemberCount(tm.TeamId, nil)
|
||||
if err != nil {
|
||||
return nil, false, MemberCountError
|
||||
}
|
||||
|
||||
if membersCount >= int64(*ts.config().TeamSettings.MaxUsersPerTeam) {
|
||||
return nil, false, MaxMemberCountError
|
||||
}
|
||||
|
||||
tm, err = applyPreSaveHooks(preSaveHooks, tm)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
member, nErr := ts.store.UpdateMember(rctx, tm)
|
||||
if nErr != nil {
|
||||
return nil, false, nErr
|
||||
}
|
||||
|
||||
return member, false, nil
|
||||
}
|
||||
|
||||
// RemoveTeamMember removes the team member from the team. This method sends
|
||||
// the websocket message before actually removing so the user being removed gets it.
|
||||
func (ts *TeamService) RemoveTeamMember(rctx request.CTX, teamMember *model.TeamMember) error {
|
||||
/*
|
||||
MM-43850: send leave_team event to user using `ReliableClusterSend` to improve safety
|
||||
*/
|
||||
// message for other team members
|
||||
omitUsers := make(map[string]bool, 1)
|
||||
omitUsers[teamMember.UserId] = true
|
||||
messageTeam := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, teamMember.TeamId, "", "", omitUsers, "")
|
||||
messageTeam.Add("user_id", teamMember.UserId)
|
||||
messageTeam.Add("team_id", teamMember.TeamId)
|
||||
ts.wh.Publish(messageTeam)
|
||||
|
||||
// message for teamMember.UserId
|
||||
messageUser := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, "", "", teamMember.UserId, nil, "")
|
||||
messageUser.Add("user_id", teamMember.UserId)
|
||||
messageUser.Add("team_id", teamMember.TeamId)
|
||||
|
||||
ts.wh.Publish(messageUser)
|
||||
|
||||
// delete team member
|
||||
teamMember.Roles = ""
|
||||
teamMember.DeleteAt = model.GetMillis()
|
||||
|
||||
if _, nErr := ts.store.UpdateMember(rctx, teamMember); nErr != nil {
|
||||
return nErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMember return the team member from the team.
|
||||
func (ts *TeamService) GetMember(rctx request.CTX, teamID string, userID string) (*model.TeamMember, error) {
|
||||
member, err := ts.store.GetMember(rctx, teamID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return member, err
|
||||
}
|
||||
202
server/channels/app/teams/teams_test.go
Normal file
202
server/channels/app/teams/teams_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package teams
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestCreateTeam(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
id := model.NewId()
|
||||
team := &model.Team{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "name" + id,
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
|
||||
_, err := th.service.CreateTeam(th.Context, team)
|
||||
require.NoError(t, err, "Should create a new team")
|
||||
|
||||
_, err = th.service.CreateTeam(th.Context, team)
|
||||
require.Error(t, err, "Should not create a new team - team already exist")
|
||||
}
|
||||
|
||||
func TestCreateTeamWithExperimentalDefaultChannels(t *testing.T) {
|
||||
th := Setup(t)
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.TeamSettings.ExperimentalDefaultChannels = []string{"channel-1", "channel-2"}
|
||||
})
|
||||
|
||||
id := model.NewId()
|
||||
team := &model.Team{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "name" + id,
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
|
||||
_, err := th.service.CreateTeam(th.Context, team)
|
||||
require.NoError(t, err, "Should create a new team")
|
||||
|
||||
createdTeam, err := th.service.GetTeam(team.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, createdTeam.Name, "name"+id)
|
||||
|
||||
channels, err := th.service.channelStore.GetAll(team.Id)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, channels, 3)
|
||||
|
||||
ch, err := th.service.channelStore.GetByName(team.Id, "town-square", false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ch)
|
||||
|
||||
ch, err = th.service.channelStore.GetByName(team.Id, "channel-1", false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ch)
|
||||
require.Equal(t, ch.DisplayName, "channel-1")
|
||||
|
||||
ch, err = th.service.channelStore.GetByName(team.Id, "channel-2", false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ch)
|
||||
require.Equal(t, ch.DisplayName, "channel-2")
|
||||
}
|
||||
|
||||
func TestJoinUserToTeam(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
id := model.NewId()
|
||||
team := &model.Team{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "name" + id,
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
|
||||
_, err := th.service.CreateTeam(th.Context, team)
|
||||
require.NoError(t, err, "Should create a new team")
|
||||
|
||||
maxUsersPerTeam := th.service.config().TeamSettings.MaxUsersPerTeam
|
||||
defer func() {
|
||||
th.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = maxUsersPerTeam })
|
||||
th.DeleteTeam(team)
|
||||
}()
|
||||
one := 1
|
||||
th.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = &one })
|
||||
|
||||
t.Run("new join", func(t *testing.T) {
|
||||
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser := th.CreateUser(&user)
|
||||
defer th.DeleteUser(&user)
|
||||
|
||||
_, alreadyAdded, err := th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.False(t, alreadyAdded, "Should return already added equal to false")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("join when you are a member", func(t *testing.T) {
|
||||
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser := th.CreateUser(&user)
|
||||
defer th.DeleteUser(&user)
|
||||
|
||||
_, _, err := th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, alreadyAdded, err := th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.True(t, alreadyAdded, "Should return already added")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("re-join after leaving", func(t *testing.T) {
|
||||
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser := th.CreateUser(&user)
|
||||
defer th.DeleteUser(&user)
|
||||
|
||||
member, _, err := th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.NoError(t, err)
|
||||
err = th.service.RemoveTeamMember(th.Context, member)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, alreadyAdded, err := th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.False(t, alreadyAdded, "Should return already added equal to false")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("guest with the team email does not become team admin", func(t *testing.T) {
|
||||
user := model.User{Email: team.Email, Nickname: "Guest Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), Roles: model.SystemGuestRoleId, AuthService: ""}
|
||||
ruser := th.CreateUser(&user)
|
||||
defer th.DeleteUser(&user)
|
||||
|
||||
member, _, err := th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.NoError(t, err)
|
||||
require.True(t, member.SchemeGuest)
|
||||
require.False(t, member.SchemeUser)
|
||||
require.False(t, member.SchemeAdmin)
|
||||
|
||||
err = th.service.RemoveTeamMember(th.Context, member)
|
||||
require.NoError(t, err)
|
||||
|
||||
member, _, err = th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.NoError(t, err)
|
||||
require.True(t, member.SchemeGuest)
|
||||
require.False(t, member.SchemeUser)
|
||||
require.False(t, member.SchemeAdmin)
|
||||
})
|
||||
|
||||
t.Run("regular user with the team email becomes team admin", func(t *testing.T) {
|
||||
user := model.User{Email: team.Email, Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser := th.CreateUser(&user)
|
||||
defer th.DeleteUser(&user)
|
||||
|
||||
member, _, err := th.service.JoinUserToTeam(th.Context, team, ruser)
|
||||
require.NoError(t, err)
|
||||
require.False(t, member.SchemeGuest)
|
||||
require.True(t, member.SchemeUser)
|
||||
require.True(t, member.SchemeAdmin)
|
||||
})
|
||||
|
||||
t.Run("new join with limit problem", func(t *testing.T) {
|
||||
user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser1 := th.CreateUser(&user1)
|
||||
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser2 := th.CreateUser(&user2)
|
||||
|
||||
defer th.DeleteUser(&user1)
|
||||
defer th.DeleteUser(&user2)
|
||||
|
||||
_, _, err := th.service.JoinUserToTeam(th.Context, team, ruser1)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = th.service.JoinUserToTeam(th.Context, team, ruser2)
|
||||
require.Error(t, err, "Should fail")
|
||||
})
|
||||
|
||||
t.Run("re-join after leaving with limit problem", func(t *testing.T) {
|
||||
user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser1 := th.CreateUser(&user1)
|
||||
|
||||
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
|
||||
ruser2 := th.CreateUser(&user2)
|
||||
|
||||
defer th.DeleteUser(&user1)
|
||||
defer th.DeleteUser(&user2)
|
||||
|
||||
member, _, err := th.service.JoinUserToTeam(th.Context, team, ruser1)
|
||||
require.NoError(t, err)
|
||||
err = th.service.RemoveTeamMember(th.Context, member)
|
||||
require.NoError(t, err)
|
||||
_, _, err = th.service.JoinUserToTeam(th.Context, team, ruser2)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = th.service.JoinUserToTeam(th.Context, team, ruser1)
|
||||
require.Error(t, err, "Should fail")
|
||||
})
|
||||
}
|
||||
110
server/channels/app/teams/utils.go
Normal file
110
server/channels/app/teams/utils.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package teams
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// By default the list will be (not necessarily in this order):
|
||||
//
|
||||
// ['town-square', 'off-topic']
|
||||
//
|
||||
// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace
|
||||
// 'off-topic' and be included in the return results in addition to 'town-square'. For example:
|
||||
//
|
||||
// ['town-square', 'game-of-thrones', 'wow']
|
||||
func (ts *TeamService) DefaultChannelNames() []string {
|
||||
names := []string{"town-square"}
|
||||
|
||||
if len(ts.config().TeamSettings.ExperimentalDefaultChannels) == 0 {
|
||||
names = append(names, "off-topic")
|
||||
} else {
|
||||
seenChannels := map[string]bool{"town-square": true}
|
||||
for _, channelName := range ts.config().TeamSettings.ExperimentalDefaultChannels {
|
||||
if !seenChannels[channelName] {
|
||||
names = append(names, channelName)
|
||||
seenChannels[channelName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func IsEmailAddressAllowed(email string, allowedDomains []string) bool {
|
||||
for _, restriction := range allowedDomains {
|
||||
domains := normalizeDomains(restriction)
|
||||
if len(domains) <= 0 {
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, d := range domains {
|
||||
if strings.HasSuffix(email, "@"+d) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ts *TeamService) IsTeamEmailAllowed(user *model.User, team *model.Team) bool {
|
||||
if user.IsBot {
|
||||
return true
|
||||
}
|
||||
email := strings.ToLower(user.Email)
|
||||
allowedDomains := ts.GetAllowedDomains(user, team)
|
||||
return IsEmailAddressAllowed(email, allowedDomains)
|
||||
}
|
||||
|
||||
func (ts *TeamService) GetAllowedDomains(user *model.User, team *model.Team) []string {
|
||||
if user.IsGuest() {
|
||||
return []string{*ts.config().GuestAccountsSettings.RestrictCreationToDomains}
|
||||
}
|
||||
// First check per team allowedDomains, then app wide restrictions
|
||||
return []string{team.AllowedDomains, *ts.config().TeamSettings.RestrictCreationToDomains}
|
||||
}
|
||||
|
||||
func (ts *TeamService) checkValidDomains(team *model.Team) error {
|
||||
validDomains := normalizeDomains(*ts.config().TeamSettings.RestrictCreationToDomains)
|
||||
if len(validDomains) > 0 {
|
||||
for _, domain := range normalizeDomains(team.AllowedDomains) {
|
||||
matched := slices.Contains(validDomains, domain)
|
||||
if !matched {
|
||||
return &DomainError{Domain: domain}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeDomains(domains string) []string {
|
||||
// commas and @ signs are optional
|
||||
// can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
|
||||
return strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
|
||||
}
|
||||
|
||||
// UserIsInAdminRoleGroup returns true at least one of the user's groups are configured to set the members as
|
||||
// admins in the given syncable.
|
||||
func (ts *TeamService) userIsInAdminRoleGroup(userID, syncableID string, syncableType model.GroupSyncableType) (bool, error) {
|
||||
groupIDs, err := ts.groupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(groupIDs) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user