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
Этот коммит содержится в:
Martin Kraft
2019-04-09 07:09:57 -04:00
коммит произвёл GitHub
родитель 43fa7e0548
Коммит 7bde0378cd
11 изменённых файлов: 629 добавлений и 2 удалений

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

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