MM-14753: Verifies that user can join teams and channels in spite of group constraints. (#10529)
* MM-147753: Verifies that users are allowed to be members of a team or a channel, based on group constraints, prior to allowing the API to add them. * MM-14753: Allow methods to return meaningful results for deleted teams or channels. * MM-14753: Renames methods to differentiate from permissions and other team and channel restrictions. * MM-14753: Only check if users are team/channel members if team/channel is group constrained. * MM-14753: Updates test function names. * MM-14753: Changes a few method signatures. * MM-14753: Small refactor and adds missing returns. * MM-14753: Changes method names from Get* to Filter* name prefixes. * MM-14753: Renames error variables. * MM-14753: Updates method names for consistency with join table names. * MM-14753: Adds case for non AppError return. * Update i18n/en.json
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
43fa7e0548
Коммит
7bde0378cd
@@ -1536,3 +1536,83 @@ func (us SqlUserStore) GetUsersBatchForIndexing(startTime, endTime int64, limit
|
||||
result.Data = usersForIndexing
|
||||
})
|
||||
}
|
||||
|
||||
func (us SqlUserStore) GetTeamGroupUsers(teamID string) store.StoreChannel {
|
||||
return store.Do(func(result *store.StoreResult) {
|
||||
query := us.usersQuery.
|
||||
Where(`Id IN (
|
||||
SELECT
|
||||
GroupMembers.UserId
|
||||
FROM
|
||||
Teams
|
||||
JOIN GroupTeams ON GroupTeams.TeamId = Teams.Id
|
||||
JOIN UserGroups ON UserGroups.Id = GroupTeams.GroupId
|
||||
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
|
||||
WHERE
|
||||
Teams.Id = ?
|
||||
AND GroupTeams.DeleteAt = 0
|
||||
AND UserGroups.DeleteAt = 0
|
||||
AND GroupMembers.DeleteAt = 0
|
||||
GROUP BY
|
||||
GroupMembers.UserId
|
||||
)`, teamID)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlUserStore.UsersPermittedToTeam", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var users []*model.User
|
||||
if _, err := us.GetReplica().Select(&users, queryString, args...); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserStore.UsersPermittedToTeam", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
u.Sanitize(map[string]bool{})
|
||||
}
|
||||
|
||||
result.Data = users
|
||||
})
|
||||
}
|
||||
|
||||
func (us SqlUserStore) GetChannelGroupUsers(channelID string) store.StoreChannel {
|
||||
return store.Do(func(result *store.StoreResult) {
|
||||
query := us.usersQuery.
|
||||
Where(`Id IN (
|
||||
SELECT
|
||||
GroupMembers.UserId
|
||||
FROM
|
||||
Channels
|
||||
JOIN GroupChannels ON GroupChannels.ChannelId = Channels.Id
|
||||
JOIN UserGroups ON UserGroups.Id = GroupChannels.GroupId
|
||||
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
|
||||
WHERE
|
||||
Channels.Id = ?
|
||||
AND GroupChannels.DeleteAt = 0
|
||||
AND UserGroups.DeleteAt = 0
|
||||
AND GroupMembers.DeleteAt = 0
|
||||
GROUP BY
|
||||
GroupMembers.UserId
|
||||
)`, channelID)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlUserStore.GetChannelGroupUsers", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var users []*model.User
|
||||
if _, err := us.GetReplica().Select(&users, queryString, args...); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserStore.GetChannelGroupUsers", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
u.Sanitize(map[string]bool{})
|
||||
}
|
||||
|
||||
result.Data = users
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,6 +294,8 @@ type UserStore interface {
|
||||
GetAllAfter(limit int, afterId string) StoreChannel
|
||||
GetUsersBatchForIndexing(startTime, endTime int64, limit int) StoreChannel
|
||||
Count(options model.UserCountOptions) StoreChannel
|
||||
GetTeamGroupUsers(teamID string) StoreChannel
|
||||
GetChannelGroupUsers(channelID string) StoreChannel
|
||||
}
|
||||
|
||||
type BotStore interface {
|
||||
|
||||
@@ -848,3 +848,35 @@ func (_m *UserStore) VerifyEmail(userId string, email string) store.StoreChannel
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetTeamGroupUsers provides a mock function with given fields: userId, email
|
||||
func (_m *UserStore) GetTeamGroupUsers(teamID string) store.StoreChannel {
|
||||
ret := _m.Called(teamID)
|
||||
|
||||
var r0 store.StoreChannel
|
||||
if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
|
||||
r0 = rf(teamID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.StoreChannel)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetChannelGroupUsers provides a mock function with given fields: userId, email
|
||||
func (_m *UserStore) GetChannelGroupUsers(teamID string) store.StoreChannel {
|
||||
ret := _m.Called(teamID)
|
||||
|
||||
var r0 store.StoreChannel
|
||||
if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
|
||||
r0 = rf(teamID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.StoreChannel)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ func TestUserStore(t *testing.T, ss store.Store) {
|
||||
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testUserStoreClearAllCustomRoleAssignments(t, ss) })
|
||||
t.Run("GetAllAfter", func(t *testing.T) { testUserStoreGetAllAfter(t, ss) })
|
||||
t.Run("GetUsersBatchForIndexing", func(t *testing.T) { testUserStoreGetUsersBatchForIndexing(t, ss) })
|
||||
t.Run("GetTeamGroupUsers", func(t *testing.T) { testUserStoreGetTeamGroupUsers(t, ss) })
|
||||
t.Run("GetChannelGroupUsers", func(t *testing.T) { testUserStoreGetChannelGroupUsers(t, ss) })
|
||||
}
|
||||
|
||||
func testUserStoreSave(t *testing.T, ss store.Store) {
|
||||
@@ -3430,3 +3432,252 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, ss store.Store) {
|
||||
assert.Equal(t, res4List[0].Username, u1.Username)
|
||||
assert.Equal(t, res4List[1].Username, u2.Username)
|
||||
}
|
||||
|
||||
func testUserStoreGetTeamGroupUsers(t *testing.T, ss store.Store) {
|
||||
// create team
|
||||
id := model.NewId()
|
||||
res := <-ss.Team().Save(&model.Team{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "n-" + id,
|
||||
Email: id + "@test.com",
|
||||
Type: model.TEAM_INVITE,
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
team := res.Data.(*model.Team)
|
||||
require.NotNil(t, team)
|
||||
|
||||
// create users
|
||||
var testUsers []*model.User
|
||||
for i := 0; i < 3; i++ {
|
||||
id = model.NewId()
|
||||
res = <-ss.User().Save(&model.User{
|
||||
Email: id + "@test.com",
|
||||
Username: "un_" + id,
|
||||
Nickname: "nn_" + id,
|
||||
FirstName: "f_" + id,
|
||||
LastName: "l_" + id,
|
||||
Password: "Password1",
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
user := res.Data.(*model.User)
|
||||
require.NotNil(t, user)
|
||||
testUsers = append(testUsers, user)
|
||||
}
|
||||
userGroupA := testUsers[0]
|
||||
userGroupB := testUsers[1]
|
||||
userNoGroup := testUsers[2]
|
||||
|
||||
// add non-group-member to the team (to prove that the query isn't just returning all members)
|
||||
res = <-ss.Team().SaveMember(&model.TeamMember{
|
||||
TeamId: team.Id,
|
||||
UserId: userNoGroup.Id,
|
||||
}, 999)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// create groups
|
||||
var testGroups []*model.Group
|
||||
for i := 0; i < 2; i++ {
|
||||
id = model.NewId()
|
||||
res = <-ss.Group().Create(&model.Group{
|
||||
Name: "n_" + id,
|
||||
DisplayName: "dn_" + id,
|
||||
Source: model.GroupSourceLdap,
|
||||
RemoteId: "ri_" + id,
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
group := res.Data.(*model.Group)
|
||||
require.NotNil(t, group)
|
||||
testGroups = append(testGroups, group)
|
||||
}
|
||||
groupA := testGroups[0]
|
||||
groupB := testGroups[1]
|
||||
|
||||
// add members to groups
|
||||
res = <-ss.Group().CreateOrRestoreMember(groupA.Id, userGroupA.Id)
|
||||
require.Nil(t, res.Err)
|
||||
res = <-ss.Group().CreateOrRestoreMember(groupB.Id, userGroupB.Id)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// association one group to team
|
||||
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: groupA.Id,
|
||||
SyncableId: team.Id,
|
||||
Type: model.GroupSyncableTypeTeam,
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
var users []*model.User
|
||||
|
||||
requireNUsers := func(n int) {
|
||||
res = <-ss.User().GetTeamGroupUsers(team.Id)
|
||||
require.Nil(t, res.Err)
|
||||
users = res.Data.([]*model.User)
|
||||
require.NotNil(t, users)
|
||||
require.Len(t, users, n)
|
||||
}
|
||||
|
||||
// team not group constrained returns users
|
||||
requireNUsers(1)
|
||||
|
||||
// update team to be group-constrained
|
||||
team.GroupConstrained = model.NewBool(true)
|
||||
res = <-ss.Team().Update(team)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// still returns user (being group-constrained has no effect)
|
||||
requireNUsers(1)
|
||||
|
||||
// associate other group to team
|
||||
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: groupB.Id,
|
||||
SyncableId: team.Id,
|
||||
Type: model.GroupSyncableTypeTeam,
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// should return users from all groups
|
||||
// 2 users now that both groups have been associated to the team
|
||||
requireNUsers(2)
|
||||
|
||||
// add team membership of allowed user
|
||||
res = <-ss.Team().SaveMember(&model.TeamMember{
|
||||
TeamId: team.Id,
|
||||
UserId: userGroupA.Id,
|
||||
}, 999)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// ensure allowed member still returned by query
|
||||
requireNUsers(2)
|
||||
|
||||
// delete team membership of allowed user
|
||||
res = <-ss.Team().RemoveMember(team.Id, userGroupA.Id)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// ensure removed allowed member still returned by query
|
||||
requireNUsers(2)
|
||||
}
|
||||
|
||||
func testUserStoreGetChannelGroupUsers(t *testing.T, ss store.Store) {
|
||||
// create channel
|
||||
id := model.NewId()
|
||||
res := <-ss.Channel().Save(&model.Channel{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "n-" + id,
|
||||
Type: model.CHANNEL_PRIVATE,
|
||||
}, 999)
|
||||
require.Nil(t, res.Err)
|
||||
channel := res.Data.(*model.Channel)
|
||||
require.NotNil(t, channel)
|
||||
|
||||
// create users
|
||||
var testUsers []*model.User
|
||||
for i := 0; i < 3; i++ {
|
||||
id = model.NewId()
|
||||
res = <-ss.User().Save(&model.User{
|
||||
Email: id + "@test.com",
|
||||
Username: "un_" + id,
|
||||
Nickname: "nn_" + id,
|
||||
FirstName: "f_" + id,
|
||||
LastName: "l_" + id,
|
||||
Password: "Password1",
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
user := res.Data.(*model.User)
|
||||
require.NotNil(t, user)
|
||||
testUsers = append(testUsers, user)
|
||||
}
|
||||
userGroupA := testUsers[0]
|
||||
userGroupB := testUsers[1]
|
||||
userNoGroup := testUsers[2]
|
||||
|
||||
// add non-group-member to the channel (to prove that the query isn't just returning all members)
|
||||
res = <-ss.Channel().SaveMember(&model.ChannelMember{
|
||||
ChannelId: channel.Id,
|
||||
UserId: userNoGroup.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// create groups
|
||||
var testGroups []*model.Group
|
||||
for i := 0; i < 2; i++ {
|
||||
id = model.NewId()
|
||||
res = <-ss.Group().Create(&model.Group{
|
||||
Name: "n_" + id,
|
||||
DisplayName: "dn_" + id,
|
||||
Source: model.GroupSourceLdap,
|
||||
RemoteId: "ri_" + id,
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
group := res.Data.(*model.Group)
|
||||
require.NotNil(t, group)
|
||||
testGroups = append(testGroups, group)
|
||||
}
|
||||
groupA := testGroups[0]
|
||||
groupB := testGroups[1]
|
||||
|
||||
// add members to groups
|
||||
res = <-ss.Group().CreateOrRestoreMember(groupA.Id, userGroupA.Id)
|
||||
require.Nil(t, res.Err)
|
||||
res = <-ss.Group().CreateOrRestoreMember(groupB.Id, userGroupB.Id)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// association one group to channel
|
||||
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: groupA.Id,
|
||||
SyncableId: channel.Id,
|
||||
Type: model.GroupSyncableTypeChannel,
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
var users []*model.User
|
||||
|
||||
requireNUsers := func(n int) {
|
||||
res = <-ss.User().GetChannelGroupUsers(channel.Id)
|
||||
require.Nil(t, res.Err)
|
||||
users = res.Data.([]*model.User)
|
||||
require.NotNil(t, users)
|
||||
require.Len(t, users, n)
|
||||
}
|
||||
|
||||
// channel not group constrained returns users
|
||||
requireNUsers(1)
|
||||
|
||||
// update team to be group-constrained
|
||||
channel.GroupConstrained = model.NewBool(true)
|
||||
res = <-ss.Channel().Update(channel)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// still returns user (being group-constrained has no effect)
|
||||
requireNUsers(1)
|
||||
|
||||
// associate other group to team
|
||||
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: groupB.Id,
|
||||
SyncableId: channel.Id,
|
||||
Type: model.GroupSyncableTypeChannel,
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// should return users from all groups
|
||||
// 2 users now that both groups have been associated to the team
|
||||
requireNUsers(2)
|
||||
|
||||
// add team membership of allowed user
|
||||
res = <-ss.Channel().SaveMember(&model.ChannelMember{
|
||||
ChannelId: channel.Id,
|
||||
UserId: userGroupA.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
})
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// ensure allowed member still returned by query
|
||||
requireNUsers(2)
|
||||
|
||||
// delete team membership of allowed user
|
||||
res = <-ss.Channel().RemoveMember(channel.Id, userGroupA.Id)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
// ensure removed allowed member still returned by query
|
||||
requireNUsers(2)
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user