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 удалений

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

@@ -717,9 +717,10 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
} }
opts := model.GroupSearchOpts{ opts := model.GroupSearchOpts{
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)

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

@@ -2994,14 +2994,15 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) {
func testGetGroups(t *testing.T, ss store.Store) { func testGetGroups(t *testing.T, ss store.Store) {
// Create Team1 // Create Team1
team1 := &model.Team{ team1 := &model.Team{
DisplayName: "Team1", DisplayName: "Team1",
Description: model.NewId(), Description: model.NewId(),
CompanyName: model.NewId(), CompanyName: model.NewId(),
AllowOpenInvite: false, AllowOpenInvite: false,
InviteId: model.NewId(), InviteId: model.NewId(),
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 {

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

@@ -23,59 +23,60 @@ const (
) )
type Params struct { type Params struct {
UserId string UserId string
TeamId string TeamId string
InviteId string InviteId string
TokenId string TokenId string
ChannelId string ChannelId string
PostId string PostId string
FileId string FileId string
Filename string Filename string
PluginId string PluginId string
CommandId string CommandId string
HookId string HookId string
ReportId string ReportId string
EmojiId string EmojiId string
AppId string AppId string
Email string Email string
Username string Username string
TeamName string TeamName string
ChannelName string ChannelName string
PreferenceName string PreferenceName string
EmojiName string EmojiName string
Category string Category string
Service string Service string
JobId string JobId string
JobType string JobType string
ActionId string ActionId string
RoleId string RoleId string
RoleName string RoleName string
SchemeId string SchemeId string
Scope string Scope string
GroupId string GroupId string
Page int Page int
PerPage int PerPage int
LogsPerPage int LogsPerPage int
Permanent bool Permanent bool
RemoteId string RemoteId string
SyncableId string SyncableId string
SyncableType model.GroupSyncableType SyncableType model.GroupSyncableType
BotUserId string BotUserId string
Q string Q string
IsLinked *bool IsLinked *bool
IsConfigured *bool IsConfigured *bool
NotAssociatedToTeam string NotAssociatedToTeam string
NotAssociatedToChannel string NotAssociatedToChannel string
Paginate *bool Paginate *bool
IncludeMemberCount bool IncludeMemberCount bool
NotAssociatedToGroup string NotAssociatedToGroup string
ExcludeDefaultChannels bool ExcludeDefaultChannels bool
LimitAfter int LimitAfter int
LimitBefore int LimitBefore int
GroupIDs string GroupIDs string
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
} }