MM-49813 - validate inviter permissions when sending invitations to private channels (#22119)

* MM-49813 - validate members invitation permissions to private channels

* Validate on the receiver side that the sender id still has permissions to invite

* add translations

* fix texts

* simplify implementation

* remove local test hardcoded data

* add test to user_test

* fix vet warning

* Add comments to validate user permissions function

Co-authored-by: Martin Kraft <martin@upspin.org>

* regenerate app layer iface

* fix unit test

* fix app user_tests

---------

Co-authored-by: Martin Kraft <martin@upspin.org>
Этот коммит содержится в:
Pablo Andrés Vélez Vidal
2023-01-30 16:19:27 +01:00
коммит произвёл GitHub
родитель e055df27e8
Коммит ae3c21dd79
9 изменённых файлов: 132 добавлений и 5 удалений

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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()

Просмотреть файл

@@ -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) {

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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) {

Просмотреть файл

@@ -704,6 +704,7 @@ func (es *Service) SendInviteEmailsToTeamAndChannels(
"teamId": team.Id,
"email": invite,
"channels": strings.Join(channelIDs, " "),
"senderId": senderUserId,
}),
)

Просмотреть файл

@@ -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")

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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{