MM-25040: Restrict associated groups to channels when team is group-constrained. (#14619)

* MM-25040: Only return team-associated groups if the team is group-constrained.
MM-25040: Prevents associating a group to a channel if the team doesn't have the group first.

* MM-25040: Fix lints.

* MM-25040: Still add the groupteam if the team is not group-constrained.

* MM-25040: Wraps groupteam upsert in else branch for efficiency.

* MM-25040: Removes unnecessary page iteration.

* MM-25040: Fix typo.

* MM-25040: Moves filtering to SQL.

* MM-25040: Updates tests, check pagination.

* MM-25040: Fix lint error.

* MM-25040: Adds some more group store tests.

* MM-25040: Fix for wrong test parameter.
Этот коммит содержится в:
Martin Kraft
2020-05-29 10:46:52 -04:00
коммит произвёл GitHub
родитель c9cdeba1a7
Коммит c529d5190a
11 изменённых файлов: 293 добавлений и 83 удалений

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

@@ -720,6 +720,7 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
Q: c.Params.Q, Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount, IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference, FilterAllowReference: c.Params.FilterAllowReference,
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
} }
if teamID != "" { if teamID != "" {

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

@@ -1007,3 +1007,86 @@ func TestGetGroupsByUserId(t *testing.T) {
assert.ElementsMatch(t, []*model.Group{group1, group2}, groups) assert.ElementsMatch(t, []*model.Group{group1, group2}, groups)
} }
func TestGetGroupsGroupConstrainedParentTeam(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.SetLicense(model.NewTestLicense("ldap"))
var groups []*model.Group
for i := 0; i < 4; i++ {
id := model.NewId()
group, err := th.App.CreateGroup(&model.Group{
DisplayName: fmt.Sprintf("dn-foo_%d", i),
Name: model.NewString("name" + id),
Source: model.GroupSourceLdap,
Description: "description_" + id,
RemoteId: model.NewId(),
})
require.Nil(t, err)
groups = append(groups, group)
}
team := th.CreateTeam()
id := model.NewId()
channel := &model.Channel{
DisplayName: "dn_" + id,
Name: "name" + id,
Type: model.CHANNEL_PRIVATE,
TeamId: team.Id,
GroupConstrained: model.NewBool(true),
}
channel, err := th.App.CreateChannel(channel, false)
require.Nil(t, err)
// normal result of groups are returned if the team is not group-constrained
apiGroups, response := th.SystemAdminClient.GetGroups(model.GroupSearchOpts{NotAssociatedToChannel: channel.Id})
require.Nil(t, response.Error)
require.Contains(t, apiGroups, groups[0])
require.Contains(t, apiGroups, groups[1])
require.Contains(t, apiGroups, groups[2])
team.GroupConstrained = model.NewBool(true)
team, err = th.App.UpdateTeam(team)
require.Nil(t, err)
// team is group-constrained but has no associated groups
apiGroups, response = th.SystemAdminClient.GetGroups(model.GroupSearchOpts{NotAssociatedToChannel: channel.Id, FilterParentTeamPermitted: true})
require.Nil(t, response.Error)
require.Len(t, apiGroups, 0)
for _, group := range []*model.Group{groups[0], groups[2], groups[3]} {
_, err = th.App.UpsertGroupSyncable(model.NewGroupTeam(group.Id, team.Id, false))
require.Nil(t, err)
}
// set of the teams groups are returned
apiGroups, response = th.SystemAdminClient.GetGroups(model.GroupSearchOpts{NotAssociatedToChannel: channel.Id, FilterParentTeamPermitted: true})
require.Nil(t, response.Error)
require.Contains(t, apiGroups, groups[0])
require.NotContains(t, apiGroups, groups[1])
require.Contains(t, apiGroups, groups[2])
// paged results function as expected
apiGroups, response = th.SystemAdminClient.GetGroups(model.GroupSearchOpts{NotAssociatedToChannel: channel.Id, FilterParentTeamPermitted: true, PageOpts: &model.PageOpts{PerPage: 2, Page: 0}})
require.Nil(t, response.Error)
require.Len(t, apiGroups, 2)
require.Equal(t, apiGroups[0].Id, groups[0].Id)
require.Equal(t, apiGroups[1].Id, groups[2].Id)
apiGroups, response = th.SystemAdminClient.GetGroups(model.GroupSearchOpts{NotAssociatedToChannel: channel.Id, FilterParentTeamPermitted: true, PageOpts: &model.PageOpts{PerPage: 2, Page: 1}})
require.Nil(t, response.Error)
require.Len(t, apiGroups, 1)
require.Equal(t, apiGroups[0].Id, groups[3].Id)
_, err = th.App.UpsertGroupSyncable(model.NewGroupChannel(groups[0].Id, channel.Id, false))
require.Nil(t, err)
// as usual it doesn't return groups already associated to the channel
apiGroups, response = th.SystemAdminClient.GetGroups(model.GroupSearchOpts{NotAssociatedToChannel: channel.Id})
require.Nil(t, response.Error)
require.NotContains(t, apiGroups, groups[0])
require.Contains(t, apiGroups, groups[2])
}

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

@@ -158,6 +158,8 @@ type AppIface interface {
GetEmojiStaticUrl(emojiName string) (string, *model.AppError) GetEmojiStaticUrl(emojiName string) (string, *model.AppError)
// GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable. // GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable.
GetEnvironmentConfig() map[string]interface{} GetEnvironmentConfig() map[string]interface{}
// GetGroupsByTeam returns the paged list and the total count of group associated to the given team.
GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError)
// GetHubForUserId returns the hub for a given user id. // GetHubForUserId returns the hub for a given user id.
GetHubForUserId(userId string) *Hub GetHubForUserId(userId string) *Hub
// GetKnownUsers returns the list of user ids of users with any direct // GetKnownUsers returns the list of user ids of users with any direct
@@ -555,7 +557,6 @@ type AppIface interface {
GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError)
GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError)
GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError)
GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError)
GetGroupsByUserId(userId string) ([]*model.Group, *model.AppError) GetGroupsByUserId(userId string) ([]*model.Group, *model.AppError)
GetIncomingWebhook(hookId string) (*model.IncomingWebhook, *model.AppError) GetIncomingWebhook(hookId string) (*model.IncomingWebhook, *model.AppError)
GetIncomingWebhooksForTeamPage(teamId string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) GetIncomingWebhooksForTeamPage(teamId string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError)

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

@@ -4,6 +4,8 @@
package app package app
import ( import (
"net/http"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
) )
@@ -86,6 +88,43 @@ func (a *App) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.Gr
return nil, err return nil, err
} }
// reject the syncable creation if the group isn't already associated to the parent team
if groupSyncable.Type == model.GroupSyncableTypeChannel {
var channel *model.Channel
channel, err = a.Srv().Store.Channel().Get(groupSyncable.SyncableId, true)
if err != nil {
return nil, err
}
var team *model.Team
team, err = a.Srv().Store.Team().Get(channel.TeamId)
if err != nil {
return nil, err
}
if team.IsGroupConstrained() {
var teamGroups []*model.GroupWithSchemeAdmin
teamGroups, err = a.Srv().Store.Group().GetGroupsByTeam(channel.TeamId, model.GroupSearchOpts{})
if err != nil {
return nil, err
}
var permittedGroup bool
for _, teamGroup := range teamGroups {
if teamGroup.Group.Id == groupSyncable.GroupId {
permittedGroup = true
break
}
}
if !permittedGroup {
return nil, model.NewAppError("App.UpsertGroupSyncable", "group_not_associated_to_synced_team", nil, "", http.StatusBadRequest)
}
} else {
_, err = a.UpsertGroupSyncable(model.NewGroupTeam(groupSyncable.GroupId, team.Id, groupSyncable.AutoAdd))
if err != nil {
return nil, err
}
}
}
if gs == nil { if gs == nil {
gs, err = a.Srv().Store.Group().CreateGroupSyncable(groupSyncable) gs, err = a.Srv().Store.Group().CreateGroupSyncable(groupSyncable)
if err != nil { if err != nil {
@@ -98,23 +137,6 @@ func (a *App) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.Gr
} }
} }
// if the type is channel, then upsert the associated GroupTeam [MM-14675]
if gs.Type == model.GroupSyncableTypeChannel {
channel, err := a.Srv().Store.Channel().Get(gs.SyncableId, true)
if err != nil {
return nil, err
}
_, err = a.UpsertGroupSyncable(&model.GroupSyncable{
GroupId: gs.GroupId,
SyncableId: channel.TeamId,
Type: model.GroupSyncableTypeTeam,
AutoAdd: gs.AutoAdd,
})
if err != nil {
return nil, err
}
}
var messageWs *model.WebSocketEvent var messageWs *model.WebSocketEvent
if gs.Type == model.GroupSyncableTypeTeam { if gs.Type == model.GroupSyncableTypeTeam {
messageWs = model.NewWebSocketEvent(model.WEBSOCKET_EVENT_RECEIVED_GROUP_ASSOCIATED_TO_TEAM, gs.SyncableId, "", "", nil) messageWs = model.NewWebSocketEvent(model.WEBSOCKET_EVENT_RECEIVED_GROUP_ASSOCIATED_TO_TEAM, gs.SyncableId, "", "", nil)
@@ -217,6 +239,7 @@ func (a *App) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) (
return groups, int(count), nil return groups, int(count), nil
} }
// GetGroupsByTeam returns the paged list and the total count of group associated to the given team.
func (a *App) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) { func (a *App) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
groups, err := a.Srv().Store.Group().GetGroupsByTeam(teamId, opts) groups, err := a.Srv().Store.Group().GetGroupsByTeam(teamId, opts)
if err != nil { if err != nil {

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

@@ -158,6 +158,33 @@ func TestUpsertGroupSyncable(t *testing.T) {
require.Equal(t, int64(0), gs.DeleteAt) require.Equal(t, int64(0), gs.DeleteAt)
} }
func TestUpsertGroupSyncableTeamGroupConstrained(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
group1 := th.CreateGroup()
group2 := th.CreateGroup()
team := th.CreateTeam()
team.GroupConstrained = model.NewBool(true)
team, err := th.App.UpdateTeam(team)
require.Nil(t, err)
_, err = th.App.UpsertGroupSyncable(model.NewGroupTeam(group1.Id, team.Id, false))
channel := th.CreateChannel(team)
_, err = th.App.UpsertGroupSyncable(model.NewGroupChannel(group2.Id, channel.Id, false))
require.NotNil(t, err)
require.Equal(t, err.Id, "group_not_associated_to_synced_team")
gs, err := th.App.GetGroupSyncable(group2.Id, channel.Id, model.GroupSyncableTypeChannel)
require.Nil(t, gs)
require.NotNil(t, err)
_, err = th.App.UpsertGroupSyncable(model.NewGroupChannel(group1.Id, channel.Id, false))
require.Nil(t, err)
}
func TestGetGroupSyncable(t *testing.T) { func TestGetGroupSyncable(t *testing.T) {
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()

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

@@ -4486,6 +4486,10 @@
"id": "ent.saml.service_disable.app_error", "id": "ent.saml.service_disable.app_error",
"translation": "SAML 2.0 is not configured or supported on this server." "translation": "SAML 2.0 is not configured or supported on this server."
}, },
{
"id": "group_not_associated_to_synced_team",
"translation": "Group cannot be associated to the channel until it is first associated to the parent group-synced team."
},
{ {
"id": "groups.unsupported_syncable_type", "id": "groups.unsupported_syncable_type",
"translation": "Unsupported syncable type '{{.Value}}'." "translation": "Unsupported syncable type '{{.Value}}'."

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

@@ -3811,13 +3811,14 @@ func (c *Client4) GetGroupsAssociatedToChannelsByTeam(teamId string, opts GroupS
// GetGroups retrieves Mattermost Groups // GetGroups retrieves Mattermost Groups
func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response) { func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response) {
path := fmt.Sprintf( path := fmt.Sprintf(
"%s?include_member_count=%v&not_associated_to_team=%v&not_associated_to_channel=%v&filter_allow_reference=%v&q=%v", "%s?include_member_count=%v&not_associated_to_team=%v&not_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v",
c.GetGroupsRoute(), c.GetGroupsRoute(),
opts.IncludeMemberCount, opts.IncludeMemberCount,
opts.NotAssociatedToTeam, opts.NotAssociatedToTeam,
opts.NotAssociatedToChannel, opts.NotAssociatedToChannel,
opts.FilterAllowReference, opts.FilterAllowReference,
opts.Q, opts.Q,
opts.FilterParentTeamPermitted,
) )
if opts.Since > 0 { if opts.Since > 0 {
path = fmt.Sprintf("%s&since=%v", path, opts.Since) path = fmt.Sprintf("%s&since=%v", path, opts.Since)

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

@@ -81,6 +81,12 @@ type GroupSearchOpts struct {
FilterAllowReference bool FilterAllowReference bool
PageOpts *PageOpts PageOpts *PageOpts
Since int64 Since int64
// FilterParentTeamPermitted filters the groups to the intersect of the
// set associated to the parent team and those returned by the query.
// If the parent team is not group-constrained or if NotAssociatedToChannel
// is not set then this option is ignored.
FilterParentTeamPermitted bool
} }
type PageOpts struct { type PageOpts struct {

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

@@ -1216,6 +1216,27 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts)
`, opts.NotAssociatedToChannel) `, opts.NotAssociatedToChannel)
} }
if opts.FilterParentTeamPermitted && len(opts.NotAssociatedToChannel) == 26 {
groupsQuery = groupsQuery.Where(`
g.Id IN (
SELECT
GroupId
FROM
GroupTeams
WHERE
GroupTeams.DeleteAt = 0
AND GroupTeams.TeamId = (
SELECT
TeamId
FROM
Channels
WHERE
Id = ?
)
)
`, opts.NotAssociatedToChannel)
}
queryString, args, err := groupsQuery.ToSql() queryString, args, err := groupsQuery.ToSql()
if err != nil { if err != nil {
return nil, model.NewAppError("SqlGroupStore.GetGroups", "store.sql_group.app_error", nil, err.Error(), http.StatusInternalServerError) return nil, model.NewAppError("SqlGroupStore.GetGroups", "store.sql_group.app_error", nil, err.Error(), http.StatusInternalServerError)

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

@@ -3002,6 +3002,7 @@ func testGetGroups(t *testing.T, ss store.Store) {
Name: "zz" + model.NewId(), Name: "zz" + model.NewId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com", Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TEAM_OPEN, Type: model.TEAM_OPEN,
GroupConstrained: model.NewBool(true),
} }
team1, err := ss.Team().Save(team1) team1, err := ss.Team().Save(team1)
require.Nil(t, err) require.Nil(t, err)
@@ -3082,6 +3083,16 @@ func testGetGroups(t *testing.T, ss store.Store) {
channel2, nErr = ss.Channel().Save(channel2, 9999) channel2, nErr = ss.Channel().Save(channel2, 9999)
require.Nil(t, nErr) require.Nil(t, nErr)
// Create Channel3
channel3 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Channel3",
Name: model.NewId(),
Type: model.CHANNEL_PRIVATE,
}
channel3, nErr = ss.Channel().Save(channel3, 9999)
require.Nil(t, nErr)
// Create Group3 // Create Group3
group3, err := ss.Group().Create(&model.Group{ group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId() + "-group-3"), Name: model.NewString(model.NewId() + "-group-3"),
@@ -3335,6 +3346,33 @@ func testGetGroups(t *testing.T, ss store.Store) {
return len(groups) == 0 return len(groups) == 0
}, },
}, },
{
Name: "Filter groups from group-constrained teams",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 2 && groups[0].Id == group1.Id && groups[1].Id == group2.Id
},
},
{
Name: "Filter groups from group-constrained page 0",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 1,
Resultf: func(groups []*model.Group) bool {
return groups[0].Id == group1.Id
},
},
{
Name: "Filter groups from group-constrained page 1",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 1,
PerPage: 1,
Resultf: func(groups []*model.Group) bool {
return groups[0].Id == group2.Id
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {

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

@@ -76,6 +76,7 @@ type Params struct {
IncludeTotalCount bool IncludeTotalCount bool
IncludeDeleted bool IncludeDeleted bool
FilterAllowReference bool FilterAllowReference bool
FilterParentTeamPermitted bool
} }
func ParamsFromRequest(r *http.Request) *Params { func ParamsFromRequest(r *http.Request) *Params {
@@ -282,6 +283,10 @@ func ParamsFromRequest(r *http.Request) *Params {
params.FilterAllowReference = val params.FilterAllowReference = val
} }
if val, err := strconv.ParseBool(query.Get("filter_parent_team_permitted")); err == nil {
params.FilterParentTeamPermitted = val
}
if val, err := strconv.ParseBool(query.Get("paginate")); err == nil { if val, err := strconv.ParseBool(query.Get("paginate")); err == nil {
params.Paginate = &val params.Paginate = &val
} }