diff --git a/api4/team.go b/api4/team.go index 08b520ebe2..49aa1d372e 100644 --- a/api4/team.go +++ b/api4/team.go @@ -1405,6 +1405,9 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("emails", emailList) if len(memberInvite.ChannelIds) > 0 { + // Check if the user sending the invitation has access to the channels where the invitation is being sent + memberInvite.ChannelIds = c.App.ValidateUserPermissionsOnChannels(c.AppContext, c.AppContext.Session().UserId, memberInvite.ChannelIds) + auditRec.AddMeta("channel_count", len(memberInvite.ChannelIds)) auditRec.AddMeta("channels", memberInvite.ChannelIds) } @@ -1521,6 +1524,9 @@ func inviteGuestsToChannels(c *Context, w http.ResponseWriter, r *http.Request) auditRec.AddMeta("channel_count", len(guestsInvite.Channels)) auditRec.AddMeta("channels", guestsInvite.Channels) + // Check if the user sending the invitation has access to the channels where the invitation is being sent + guestsInvite.Channels = c.App.ValidateUserPermissionsOnChannels(c.AppContext, c.AppContext.Session().UserId, guestsInvite.Channels) + if graceful { var invitesWithError []*model.EmailInviteWithError var appErr *model.AppError diff --git a/api4/team_test.go b/api4/team_test.go index d744042230..35a03392e3 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -4,6 +4,7 @@ package api4 import ( + "context" "encoding/base64" "encoding/binary" "encoding/json" @@ -17,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v6/app" + "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/einterfaces/mocks" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" @@ -3157,6 +3159,37 @@ func TestImportTeam(t *testing.T) { }) } +func TestValidateUserPermissionsOnChannels(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // define user session and context + context := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) + + t.Run("User WITH permissions on private channel CAN invite members to it", func(t *testing.T) { + channelIds := []string{th.BasicChannel.Id, th.BasicPrivateChannel.Id} + + require.Len(t, channelIds, 2) + + channelIds = th.App.ValidateUserPermissionsOnChannels(context, th.BasicUser.Id, channelIds) + + // basicUser has permission onBasicChannel and BasicPrivateChannel so he can invite to both channels + require.Len(t, channelIds, 2) + }) + + t.Run("User WITHOUT permissions on private channel CAN NOT invite members to it", func(t *testing.T) { + channelIdWithoutPermissions := th.BasicPrivateChannel2.Id + channelIds := []string{th.BasicChannel.Id, channelIdWithoutPermissions} + + require.Len(t, channelIds, 2) + + channelIds = th.App.ValidateUserPermissionsOnChannels(context, th.BasicUser.Id, channelIds) + + // basicUser DOES NOT have permission on BasicPrivateChannel2 so he can only invite to BasicChannel + require.Len(t, channelIds, 1) + }) +} + func TestInviteUsersToTeam(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/api4/user_test.go b/api4/user_test.go index a20ae682a1..dd5dfcd537 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -382,6 +382,44 @@ func TestCreateUserWithToken(t *testing.T) { _, err = th.App.Srv().Store().Token().GetByToken(token.Token) require.Error(t, err, "The token must be deleted after be used") }) + + t.Run("Validate inviter user has permissions on channels he is inviting", func(t *testing.T) { + user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: "hello1", Username: GenerateTestUsername(), Roles: model.SystemUserRoleId} + channelIdWithoutPermissions := th.BasicPrivateChannel2.Id + channelIds := th.BasicChannel.Id + " " + channelIdWithoutPermissions + token := model.NewToken( + app.TokenTypeTeamInvitation, + model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email, "senderId": th.BasicUser.Id, "channels": channelIds}), + ) + require.NoError(t, th.App.Srv().Store().Token().Save(token)) + + ruser, resp, err := th.Client.CreateUserWithToken(&user, token.Token) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + + th.Client.Login(user.Email, user.Password) + require.Equal(t, user.Nickname, ruser.Nickname) + require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles") + CheckUserSanitization(t, ruser) + _, err = th.App.Srv().Store().Token().GetByToken(token.Token) + require.Error(t, err, "The token must be deleted after being used") + + teams, appErr := th.App.GetTeamsForUser(ruser.Id) + require.Nil(t, appErr) + require.NotEmpty(t, teams, "The user must have teams") + require.Equal(t, th.BasicTeam.Id, teams[0].Id, "The user joined team must be the team provided.") + + // Now we get all the channels for the just created user + channelList, cErr := th.App.GetChannelsForTeamForUser(th.Context, th.BasicTeam.Id, ruser.Id, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) + require.Nil(t, cErr) + + // basicUser has no permissions on BasicPrivateChannel2 so the new invited user should be able to only access + // one channel from the two he was invited (plus the two default channels) + require.Len(t, channelList, 3) + }) } func TestCreateUserWebSocketEvent(t *testing.T) { diff --git a/app/app_iface.go b/app/app_iface.go index 83ff0cc7f5..81b4a9004f 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -394,6 +394,8 @@ type AppIface interface { // UserIsInAdminRoleGroup returns true at least one of the user's groups are configured to set the members as // admins in the given syncable. UserIsInAdminRoleGroup(userID, syncableID string, syncableType model.GroupSyncableType) (bool, *model.AppError) + // ValidateUserPermissionsOnChannels filters channelIds based on whether userId is authorized to manage channel members. Unauthorized channels are removed from the returned list. + ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string // VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate. VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError AccountMigration() einterfaces.AccountMigrationInterface diff --git a/app/channel.go b/app/channel.go index 3239fc9466..75105bc4e7 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2651,6 +2651,28 @@ func (a *App) IsCRTEnabledForUser(c request.CTX, userID string) bool { return threadsEnabled } +// ValidateUserPermissionsOnChannels filters channelIds based on whether userId is authorized to manage channel members. Unauthorized channels are removed from the returned list. +func (a *App) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string { + var allowedChannelIds []string + + for _, channelId := range channelIds { + channel, err := a.GetChannel(c, channelId) + if err != nil { + mlog.Info("Invite users to team - couldn't get channel " + channelId) + continue + } + + if channel.Type == model.ChannelTypePrivate && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePrivateChannelMembers) { + allowedChannelIds = append(allowedChannelIds, channelId) + } else if channel.Type == model.ChannelTypeOpen && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePublicChannelMembers) { + allowedChannelIds = append(allowedChannelIds, channelId) + } else { + mlog.Info("Invite users to team - no permission to add members to that channel. UserId: " + userId + " ChannelId: " + channelId) + } + } + return allowedChannelIds +} + // MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one. func (a *App) MarkChannelAsUnreadFromPost(c request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) { if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(c, userID) { diff --git a/app/email/email.go b/app/email/email.go index 5a77ab0813..b24da7806c 100644 --- a/app/email/email.go +++ b/app/email/email.go @@ -704,6 +704,7 @@ func (es *Service) SendInviteEmailsToTeamAndChannels( "teamId": team.Id, "email": invite, "channels": strings.Join(channelIDs, " "), + "senderId": senderUserId, }), ) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index e82946c065..882337ed98 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -18577,6 +18577,23 @@ func (a *OpenTracingAppLayer) UserIsInAdminRoleGroup(userID string, syncableID s return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateUserPermissionsOnChannels") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.ValidateUserPermissionsOnChannels(c, userId, channelIds) + + return resultVar0 +} + func (a *OpenTracingAppLayer) VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.VerifyEmailFromToken") diff --git a/app/user.go b/app/user.go index b59774fd13..0f9f5fb5dc 100644 --- a/app/user.go +++ b/app/user.go @@ -70,7 +70,15 @@ func (a *App) CreateUserWithToken(c request.CTX, user *model.User, token *model. } } - channels, nErr := a.Srv().Store().Channel().GetChannelsByIds(strings.Split(tokenData["channels"], " "), false) + // find the sender id and grab the channels in order to validate + // the sender id still belongs to team and to private channels + senderId := tokenData["senderId"] + channelIds := strings.Split(tokenData["channels"], " ") + + // filter the channels the original inviter has still permissions over + channelIds = a.ValidateUserPermissionsOnChannels(c, senderId, channelIds) + + channels, nErr := a.Srv().Store().Channel().GetChannelsByIds(channelIds, false) if nErr != nil { return nil, model.NewAppError("CreateUserWithToken", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } diff --git a/app/user_test.go b/app/user_test.go index b4a57dd32a..90927f9f16 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -922,7 +922,7 @@ func TestCreateUserWithToken(t *testing.T) { invitationEmail := strings.ToLower(model.NewId()) + "other-email@test.com" token := model.NewToken( TokenTypeGuestInvitation, - model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail, "channels": th.BasicChannel.Id}), + model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}), ) require.NoError(t, th.App.Srv().Store().Token().Save(token)) @@ -953,11 +953,11 @@ func TestCreateUserWithToken(t *testing.T) { grantedInvitationEmail := strings.ToLower(model.NewId()) + "other-email@restricted.com" forbiddenDomainToken := model.NewToken( TokenTypeGuestInvitation, - model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": forbiddenInvitationEmail, "channels": th.BasicChannel.Id}), + model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": forbiddenInvitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}), ) grantedDomainToken := model.NewToken( TokenTypeGuestInvitation, - model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": grantedInvitationEmail, "channels": th.BasicChannel.Id}), + model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": grantedInvitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}), ) require.NoError(t, th.App.Srv().Store().Token().Save(forbiddenDomainToken)) require.NoError(t, th.App.Srv().Store().Token().Save(grantedDomainToken)) @@ -1001,7 +1001,7 @@ func TestCreateUserWithToken(t *testing.T) { invitationEmail := strings.ToLower(model.NewId()) + "other-email@test.com" token := model.NewToken( TokenTypeGuestInvitation, - model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail, "channels": th.BasicChannel.Id}), + model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}), ) require.NoError(t, th.App.Srv().Store().Token().Save(token)) guest := model.User{