diff --git a/api4/group.go b/api4/group.go index 5d6fdc7402..dcf088c1ce 100644 --- a/api4/group.go +++ b/api4/group.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "github.com/mattermost/mattermost-server/v5/audit" "github.com/mattermost/mattermost-server/v5/model" @@ -61,6 +62,10 @@ func (api *API) InitGroup() { // GET /api/v4/teams/:team_id/groups?page=0&per_page=100 api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.ApiSessionRequired(getGroupsByTeam)).Methods("GET") + + // GET /api/v4/teams/:team_id/groups_by_channels?page=0&per_page=100 + api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels", + api.ApiSessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods("GET") } func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { @@ -126,6 +131,13 @@ func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) { } auditRec.AddMeta("group", group) + if groupPatch.AllowReference != nil && *groupPatch.AllowReference { + tmp := model.NewId() + if groupPatch.Name == nil { + tmp = strings.ReplaceAll(strings.ToLower(group.DisplayName), " ", "-") + } + groupPatch.Name = &tmp + } group.Patch(groupPatch) group, err = c.App.UpdateGroup(group) @@ -521,8 +533,9 @@ func getGroupsByChannel(c *Context, w http.ResponseWriter, r *http.Request) { } opts := model.GroupSearchOpts{ - Q: c.Params.Q, - IncludeMemberCount: c.Params.IncludeMemberCount, + Q: c.Params.Q, + IncludeMemberCount: c.Params.IncludeMemberCount, + FilterAllowReference: c.Params.FilterAllowReference, } if c.Params.Paginate == nil || *c.Params.Paginate { opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage} @@ -561,14 +574,10 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToTeam(*c.App.Session(), c.Params.TeamId, model.PERMISSION_MANAGE_TEAM) { - c.SetPermissionError(model.PERMISSION_MANAGE_TEAM) - return - } - opts := model.GroupSearchOpts{ - Q: c.Params.Q, - IncludeMemberCount: c.Params.IncludeMemberCount, + Q: c.Params.Q, + IncludeMemberCount: c.Params.IncludeMemberCount, + FilterAllowReference: c.Params.FilterAllowReference, } if c.Params.Paginate == nil || *c.Params.Paginate { opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage} @@ -596,6 +605,46 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(b) } +func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + if c.App.License() == nil || !*c.App.License().Features.LDAPGroups { + c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) + return + } + + opts := model.GroupSearchOpts{ + Q: c.Params.Q, + IncludeMemberCount: c.Params.IncludeMemberCount, + FilterAllowReference: c.Params.FilterAllowReference, + } + if c.Params.Paginate == nil || *c.Params.Paginate { + opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage} + } + + groupsAssociatedByChannelID, err := c.App.GetGroupsAssociatedToChannelsByTeam(c.Params.TeamId, opts) + if err != nil { + c.Err = err + return + } + + b, marshalErr := json.Marshal(struct { + GroupsAssociatedToChannels map[string][]*model.GroupWithSchemeAdmin `json:"groups"` + }{ + GroupsAssociatedToChannels: groupsAssociatedByChannelID, + }) + + if marshalErr != nil { + c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) + return + } + + w.Write(b) +} + func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { if c.App.License() == nil || !*c.App.License().Features.LDAPGroups { c.Err = model.NewAppError("Api4.getGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented) @@ -611,14 +660,10 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { channelID = id } - if teamID == "" && channelID == "" && !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { - c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) - return - } - opts := model.GroupSearchOpts{ - Q: c.Params.Q, - IncludeMemberCount: c.Params.IncludeMemberCount, + Q: c.Params.Q, + IncludeMemberCount: c.Params.IncludeMemberCount, + FilterAllowReference: c.Params.FilterAllowReference, } if teamID != "" { @@ -627,10 +672,7 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = err return } - if !c.App.SessionHasPermissionToTeam(*c.App.Session(), teamID, model.PERMISSION_MANAGE_TEAM) { - c.SetPermissionError(model.PERMISSION_MANAGE_TEAM) - return - } + opts.NotAssociatedToTeam = teamID } diff --git a/api4/group_test.go b/api4/group_test.go index e5e896651e..1ade5f7e46 100644 --- a/api4/group_test.go +++ b/api4/group_test.go @@ -711,6 +711,80 @@ func TestGetGroupsByChannel(t *testing.T) { assert.Empty(t, groups) } +func TestGetGroupsAssociatedToChannelsByTeam(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + id := model.NewId() + group, err := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + id, + Name: "name" + id, + Source: model.GroupSourceLdap, + Description: "description_" + id, + RemoteId: model.NewId(), + }) + assert.Nil(t, err) + + groupSyncable, err := th.App.UpsertGroupSyncable(&model.GroupSyncable{ + AutoAdd: true, + SyncableId: th.BasicChannel.Id, + Type: model.GroupSyncableTypeChannel, + GroupId: group.Id, + }) + assert.Nil(t, err) + + opts := model.GroupSearchOpts{ + PageOpts: &model.PageOpts{ + Page: 0, + PerPage: 60, + }, + } + + _, response := th.SystemAdminClient.GetGroupsAssociatedToChannelsByTeam("asdfasdf", opts) + CheckBadRequestStatus(t, response) + + th.App.SetLicense(nil) + + _, response = th.SystemAdminClient.GetGroupsAssociatedToChannelsByTeam(th.BasicTeam.Id, opts) + CheckNotImplementedStatus(t, response) + + th.App.SetLicense(model.NewTestLicense("ldap")) + + groups, response := th.SystemAdminClient.GetGroupsAssociatedToChannelsByTeam(th.BasicTeam.Id, opts) + assert.Nil(t, response.Error) + + assert.Equal(t, map[string][]*model.GroupWithSchemeAdmin{ + th.BasicChannel.Id: { + {Group: *group, SchemeAdmin: model.NewBool(false)}, + }, + }, groups) + + require.NotNil(t, groups[th.BasicChannel.Id][0].SchemeAdmin) + require.False(t, *groups[th.BasicChannel.Id][0].SchemeAdmin) + + // set syncable to true + groupSyncable.SchemeAdmin = true + _, err = th.App.UpdateGroupSyncable(groupSyncable) + require.Nil(t, err) + + // ensure that SchemeAdmin field is updated + groups, response = th.SystemAdminClient.GetGroupsAssociatedToChannelsByTeam(th.BasicTeam.Id, opts) + assert.Nil(t, response.Error) + + assert.Equal(t, map[string][]*model.GroupWithSchemeAdmin{ + th.BasicChannel.Id: { + {Group: *group, SchemeAdmin: model.NewBool(true)}, + }, + }, groups) + + require.NotNil(t, groups[th.BasicChannel.Id][0].SchemeAdmin) + require.True(t, *groups[th.BasicChannel.Id][0].SchemeAdmin) + + groups, response = th.SystemAdminClient.GetGroupsAssociatedToChannelsByTeam(model.NewId(), opts) + assert.Nil(t, response.Error) + assert.Empty(t, groups) +} + func TestGetGroupsByTeam(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() @@ -750,9 +824,6 @@ func TestGetGroupsByTeam(t *testing.T) { th.App.SetLicense(model.NewTestLicense("ldap")) - _, _, response = th.Client.GetGroupsByTeam(th.BasicTeam.Id, opts) - CheckForbiddenStatus(t, response) - groups, _, response := th.SystemAdminClient.GetGroupsByTeam(th.BasicTeam.Id, opts) assert.Nil(t, response.Error) assert.ElementsMatch(t, []*model.GroupWithSchemeAdmin{{Group: *group, SchemeAdmin: model.NewBool(false)}}, groups) @@ -812,9 +883,6 @@ func TestGetGroups(t *testing.T) { opts.NotAssociatedToChannel = th.BasicChannel.Id - _, response = th.Client.GetGroups(opts) - CheckForbiddenStatus(t, response) - _, response = th.SystemAdminClient.UpdateChannelRoles(th.BasicChannel.Id, th.BasicUser.Id, "channel_user channel_admin") require.Nil(t, response.Error) @@ -837,17 +905,10 @@ func TestGetGroups(t *testing.T) { require.Nil(t, response.Error) opts.NotAssociatedToTeam = th.BasicTeam.Id - _, response = th.Client.GetGroups(opts) - CheckForbiddenStatus(t, response) _, response = th.SystemAdminClient.UpdateTeamMemberRoles(th.BasicTeam.Id, th.BasicUser.Id, "team_user team_admin") require.Nil(t, response.Error) _, response = th.Client.GetGroups(opts) assert.Nil(t, response.Error) - - opts.NotAssociatedToTeam = "" - opts.NotAssociatedToChannel = "" - _, response = th.Client.GetGroups(opts) - CheckForbiddenStatus(t, response) } diff --git a/app/app_iface.go b/app/app_iface.go index b264b8b686..a8874fbe18 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -517,6 +517,7 @@ type AppIface interface { GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) + GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) diff --git a/app/group.go b/app/group.go index 0a4dfb0978..9bed101e6f 100644 --- a/app/group.go +++ b/app/group.go @@ -196,6 +196,15 @@ func (a *App) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*mod return groups, int(count), nil } +func (a *App) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) { + groupsAssociatedByChannelId, err := a.Srv().Store.Group().GetGroupsAssociatedToChannelsByTeam(teamId, opts) + if err != nil { + return nil, err + } + + return groupsAssociatedByChannelId, nil +} + func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) { return a.Srv().Store.Group().GetGroups(page, perPage, opts) } diff --git a/app/group_test.go b/app/group_test.go index fa928c32d9..d3a6e9412a 100644 --- a/app/group_test.go +++ b/app/group_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/mattermost/mattermost-server/v5/model" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -243,6 +244,45 @@ func TestGetGroupsByChannel(t *testing.T) { require.Empty(t, groups) } +func TestGetGroupsAssociatedToChannelsByTeam(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + group := th.CreateGroup() + + // Create a group channel + groupSyncable := &model.GroupSyncable{ + GroupId: group.Id, + AutoAdd: false, + SyncableId: th.BasicChannel.Id, + Type: model.GroupSyncableTypeChannel, + } + + gs, err := th.App.UpsertGroupSyncable(groupSyncable) + require.Nil(t, err) + require.NotNil(t, gs) + + opts := model.GroupSearchOpts{ + PageOpts: &model.PageOpts{ + Page: 0, + PerPage: 60, + }, + } + + groups, err := th.App.GetGroupsAssociatedToChannelsByTeam(th.BasicTeam.Id, opts) + require.Nil(t, err) + + assert.Equal(t, map[string][]*model.GroupWithSchemeAdmin{ + th.BasicChannel.Id: { + {Group: *group, SchemeAdmin: model.NewBool(false)}, + }, + }, groups) + require.NotNil(t, groups[th.BasicChannel.Id][0].SchemeAdmin) + + groups, err = th.App.GetGroupsAssociatedToChannelsByTeam(model.NewId(), opts) + require.Nil(t, err) + require.Empty(t, groups) +} + func TestGetGroupsByTeam(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/opentracing_layer.go b/app/opentracing_layer.go index 227bf4174b..912ee630eb 100644 --- a/app/opentracing_layer.go +++ b/app/opentracing_layer.go @@ -5333,6 +5333,28 @@ func (a *OpenTracingAppLayer) GetGroups(page int, perPage int, opts model.GroupS return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsAssociatedToChannelsByTeam") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetGroupsAssociatedToChannelsByTeam(teamId, opts) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByChannel") diff --git a/model/client4.go b/model/client4.go index df0d0e4a9c..95c62908ce 100644 --- a/model/client4.go +++ b/model/client4.go @@ -3679,7 +3679,7 @@ func (c *Client4) UnlinkLdapGroup(dn string) (*Group, *Response) { // GetGroupsByChannel retrieves the Mattermost Groups associated with a given channel func (c *Client4) GetGroupsByChannel(channelId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response) { - path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v", c.GetChannelRoute(channelId), opts.Q, opts.IncludeMemberCount) + path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.GetChannelRoute(channelId), opts.Q, opts.IncludeMemberCount, opts.FilterAllowReference) if opts.PageOpts != nil { path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage) } @@ -3703,7 +3703,7 @@ func (c *Client4) GetGroupsByChannel(channelId string, opts GroupSearchOpts) ([] // GetGroupsByTeam retrieves the Mattermost Groups associated with a given team func (c *Client4) GetGroupsByTeam(teamId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response) { - path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v", c.GetTeamRoute(teamId), opts.Q, opts.IncludeMemberCount) + path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.GetTeamRoute(teamId), opts.Q, opts.IncludeMemberCount, opts.FilterAllowReference) if opts.PageOpts != nil { path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage) } @@ -3725,14 +3725,38 @@ func (c *Client4) GetGroupsByTeam(teamId string, opts GroupSearchOpts) ([]*Group return responseData.Groups, responseData.Count, BuildResponse(r) } +// GetGroupsAssociatedToChannelsByTeam retrieves the Mattermost Groups associated with channels in a given team +func (c *Client4) GetGroupsAssociatedToChannelsByTeam(teamId string, opts GroupSearchOpts) (map[string][]*GroupWithSchemeAdmin, *Response) { + path := fmt.Sprintf("%s/groups_by_channels?q=%v&filter_allow_reference=%v", c.GetTeamRoute(teamId), opts.Q, opts.FilterAllowReference) + if opts.PageOpts != nil { + path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage) + } + r, appErr := c.DoApiGet(path, "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + responseData := struct { + GroupsAssociatedToChannels map[string][]*GroupWithSchemeAdmin `json:"groups"` + }{} + if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil { + appErr := NewAppError("Api4.GetGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + return nil, BuildErrorResponse(r, appErr) + } + + return responseData.GroupsAssociatedToChannels, BuildResponse(r) +} + // GetGroups retrieves Mattermost Groups func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response) { path := fmt.Sprintf( - "%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&q=%v", + "%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v", c.GetGroupsRoute(), opts.IncludeMemberCount, opts.NotAssociatedToTeam, opts.NotAssociatedToChannel, + opts.FilterAllowReference, opts.Q, ) if opts.PageOpts != nil { diff --git a/model/group.go b/model/group.go index ec69c4041b..25a93cebf9 100644 --- a/model/group.go +++ b/model/group.go @@ -30,17 +30,18 @@ var groupSourcesRequiringRemoteID = []GroupSource{ } type Group struct { - Id string `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - Description string `json:"description"` - Source GroupSource `json:"source"` - RemoteId string `json:"remote_id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - HasSyncables bool `db:"-" json:"has_syncables"` - MemberCount *int `db:"-" json:"member_count,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Source GroupSource `json:"source"` + RemoteId string `json:"remote_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + HasSyncables bool `db:"-" json:"has_syncables"` + MemberCount *int `db:"-" json:"member_count,omitempty"` + AllowReference bool `json:"allow_reference"` } type GroupWithSchemeAdmin struct { @@ -48,10 +49,21 @@ type GroupWithSchemeAdmin struct { SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"` } +type GroupsAssociatedToChannelWithSchemeAdmin struct { + ChannelId string `json:"channel_id"` + Group + SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"` +} +type GroupsAssociatedToChannel struct { + ChannelId string `json:"channel_id"` + Groups []*GroupWithSchemeAdmin `json:"groups"` +} + type GroupPatch struct { - Name *string `json:"name"` - DisplayName *string `json:"display_name"` - Description *string `json:"description"` + Name *string `json:"name"` + DisplayName *string `json:"display_name"` + Description *string `json:"description"` + AllowReference *bool `json:"allow_reference"` } type LdapGroupSearchOpts struct { @@ -65,6 +77,7 @@ type GroupSearchOpts struct { NotAssociatedToTeam string NotAssociatedToChannel string IncludeMemberCount bool + FilterAllowReference bool PageOpts *PageOpts } @@ -83,6 +96,9 @@ func (group *Group) Patch(patch *GroupPatch) { if patch.Description != nil { group.Description = *patch.Description } + if patch.AllowReference != nil { + group.AllowReference = *patch.AllowReference + } } func (group *Group) IsValidForCreate() *AppError { diff --git a/store/sqlstore/group_store.go b/store/sqlstore/group_store.go index 71ef317d01..66105efa5c 100644 --- a/store/sqlstore/group_store.go +++ b/store/sqlstore/group_store.go @@ -909,6 +909,77 @@ func (s *SqlGroupStore) groupsBySyncableBaseQuery(st model.GroupSyncableType, t OrderBy("ug.DisplayName") } + if opts.FilterAllowReference && t == selectGroups { + query = query.Where("ug.AllowReference = true") + } + + if len(opts.Q) > 0 { + pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\")) + operatorKeyword := "ILIKE" + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + operatorKeyword = "LIKE" + } + query = query.Where(fmt.Sprintf("(ug.Name %[1]s ? OR ug.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern) + } + + return query +} + +func (s *SqlGroupStore) getGroupsAssociatedToChannelsByTeam(st model.GroupSyncableType, teamID string, opts model.GroupSearchOpts) sq.SelectBuilder { + query := s.getQueryBuilder(). + Select("gc.ChannelId, ug.*, gc.SchemeAdmin AS SyncableSchemeAdmin"). + From("UserGroups ug"). + LeftJoin(fmt.Sprintf(`( + SELECT + GroupChannels.GroupId, GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.SchemeAdmin + FROM + GroupChannels + LEFT JOIN + Channels ON (Channels.Id = GroupChannels.ChannelId) + WHERE + GroupChannels.DeleteAt = 0 + AND Channels.DeleteAt = 0 + AND Channels.TeamId = ?) AS gc + ON gc.GroupId = ug.Id`), teamID). + Where("ug.DeleteAt = 0 AND gc.DeleteAt = 0"). + OrderBy("ug.DisplayName") + + if opts.IncludeMemberCount { + query = s.getQueryBuilder(). + Select("gc.ChannelId, ug.*, coalesce(Members.MemberCount, 0) AS MemberCount, gc.SchemeAdmin AS SyncableSchemeAdmin"). + From("UserGroups ug"). + LeftJoin(fmt.Sprintf(`( + SELECT + GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.GroupId, GroupChannels.SchemeAdmin + FROM + GroupChannels + LEFT JOIN + Channels ON (Channels.Id = GroupChannels.ChannelId) + WHERE + GroupChannels.DeleteAt = 0 + AND Channels.DeleteAt = 0 + AND Channels.TeamId = ?) AS gc + ON gc.GroupId = ug.Id`), teamID). + LeftJoin(`( + SELECT + GroupMembers.GroupId, COUNT(*) AS MemberCount + FROM + GroupMembers + LEFT JOIN + Users ON Users.Id = GroupMembers.UserId + WHERE + GroupMembers.DeleteAt = 0 + AND Users.DeleteAt = 0 + GROUP BY GroupId) AS Members + ON Members.GroupId = ug.Id`). + Where("ug.DeleteAt = 0 AND gc.DeleteAt = 0"). + OrderBy("ug.DisplayName") + } + + if opts.FilterAllowReference { + query = query.Where("ug.AllowReference = true") + } + if len(opts.Q) > 0 { pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\")) operatorKeyword := "ILIKE" @@ -960,6 +1031,42 @@ func (s *SqlGroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpt return groups, nil } +func (s *SqlGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) { + query := s.getGroupsAssociatedToChannelsByTeam(model.GroupSyncableTypeTeam, teamId, opts) + + if opts.PageOpts != nil { + offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage) + query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset) + } + + queryString, args, err := query.ToSql() + if err != nil { + return nil, model.NewAppError("SqlGroupStore.GetGroupsAssociatedToChannelsByTeam", "store.sql_group.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var tgroups []*model.GroupsAssociatedToChannelWithSchemeAdmin + + _, err = s.GetReplica().Select(&tgroups, queryString, args...) + if err != nil { + return nil, model.NewAppError("SqlGroupStore.GetGroupsAssociatedToChannelsByTeam", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + + groups := map[string][]*model.GroupWithSchemeAdmin{} + for _, tgroup := range tgroups { + var group = model.GroupWithSchemeAdmin{} + group.Group = tgroup.Group + group.SchemeAdmin = tgroup.SchemeAdmin + + if val, ok := groups[tgroup.ChannelId]; ok { + groups[tgroup.ChannelId] = append(val, &group) + } else { + groups[tgroup.ChannelId] = []*model.GroupWithSchemeAdmin{&group} + } + } + + return groups, nil +} + func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) { var groups []*model.Group @@ -978,6 +1085,10 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts) Offset(uint64(page * perPage)). OrderBy("g.DisplayName") + if opts.FilterAllowReference { + groupsQuery = groupsQuery.Where("g.AllowReference = true") + } + if len(opts.Q) > 0 { pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\")) operatorKeyword := "ILIKE" diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 8eac0a3c1a..a92e1e196c 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -176,7 +176,7 @@ func upgradeDatabase(sqlStore SqlStore, currentModelVersionString string) error upgradeDatabaseToVersion521(sqlStore) upgradeDatabaseToVersion522(sqlStore) upgradeDatabaseToVersion523(sqlStore) - + upgradeDatabaseToVersion524(sqlStore) return nil } @@ -793,3 +793,13 @@ func upgradeDatabaseToVersion523(sqlStore SqlStore) { // saveSchemaVersion(sqlStore, VERSION_5_23_0) // } } + +func upgradeDatabaseToVersion524(sqlStore SqlStore) { + // TODO: uncomment when the time arrive to upgrade the DB for 5.24 + // if shouldPerformUpgrade(sqlStore, VERSION_5_23_0, VERSION_5_24_0) { + + sqlStore.CreateColumnIfNotExists("UserGroups", "AllowReference", "boolean", "boolean", "0") + + // saveSchemaVersion(sqlStore, VERSION_5_24_0) + // } +} diff --git a/store/store.go b/store/store.go index 7093c89db1..868e3ef38b 100644 --- a/store/store.go +++ b/store/store.go @@ -661,6 +661,7 @@ type GroupStore interface { CountGroupsByChannel(channelId string, opts model.GroupSearchOpts) (int64, *model.AppError) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, *model.AppError) + GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, *model.AppError) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) diff --git a/store/storetest/group_store.go b/store/storetest/group_store.go index b923df16aa..14e34af541 100644 --- a/store/storetest/group_store.go +++ b/store/storetest/group_store.go @@ -55,6 +55,7 @@ func TestGroupStore(t *testing.T, ss store.Store) { t.Run("ChannelMembersToRemove_SingleChannel", func(t *testing.T) { testChannelMembersToRemoveSingleChannel(t, ss) }) t.Run("GetGroupsByChannel", func(t *testing.T) { testGetGroupsByChannel(t, ss) }) + t.Run("GetGroupsAssociatedToChannelsByTeam", func(t *testing.T) { testGetGroupsAssociatedToChannelsByTeam(t, ss) }) t.Run("GetGroupsByTeam", func(t *testing.T) { testGetGroupsByTeam(t, ss) }) t.Run("GetGroups", func(t *testing.T) { testGetGroups(t, ss) }) @@ -2055,27 +2056,30 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { // Create Groups 1, 2 and a deleted group group1, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-1", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId(), + DisplayName: "group-1", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, }) require.Nil(t, err) group2, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-2", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId(), + DisplayName: "group-2", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: false, }) require.Nil(t, err) deletedGroup, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-deleted", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, - DeleteAt: 1, + Name: model.NewId(), + DisplayName: "group-deleted", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, + DeleteAt: 1, }) require.Nil(t, err) @@ -2102,10 +2106,11 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { // Create Group3 group3, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-3", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId(), + DisplayName: "group-3", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, }) require.Nil(t, err) @@ -2242,6 +2247,14 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { {Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)}, }, }, + { + Name: "Include allow reference", + ChannelId: channel1.Id, + Opts: model.GroupSearchOpts{FilterAllowReference: true}, + Page: 0, + PerPage: 100, + Result: []*model.GroupWithSchemeAdmin{group1WSA}, + }, } for _, tc := range testCases { @@ -2264,6 +2277,249 @@ func testGetGroupsByChannel(t *testing.T, ss store.Store) { } } +func testGetGroupsAssociatedToChannelsByTeam(t *testing.T, ss store.Store) { + // Create Team1 + team1 := &model.Team{ + DisplayName: "Team1", + Description: model.NewId(), + CompanyName: model.NewId(), + AllowOpenInvite: false, + InviteId: model.NewId(), + Name: "zz" + model.NewId(), + Email: "success+" + model.NewId() + "@simulator.amazonses.com", + Type: model.TEAM_OPEN, + } + team1, errt := ss.Team().Save(team1) + require.Nil(t, errt) + + // Create Channel1 + channel1 := &model.Channel{ + TeamId: team1.Id, + DisplayName: "Channel1", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + } + channel1, err := ss.Channel().Save(channel1, 9999) + require.Nil(t, err) + + // Create Groups 1, 2 and a deleted group + group1, err := ss.Group().Create(&model.Group{ + Name: model.NewId(), + DisplayName: "group-1", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: false, + }) + require.Nil(t, err) + + group2, err := ss.Group().Create(&model.Group{ + Name: model.NewId(), + DisplayName: "group-2", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, + }) + require.Nil(t, err) + + deletedGroup, err := ss.Group().Create(&model.Group{ + Name: model.NewId(), + DisplayName: "group-deleted", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, + DeleteAt: 1, + }) + require.Nil(t, err) + + // And associate them with Channel1 + for _, g := range []*model.Group{group1, group2, deletedGroup} { + _, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{ + AutoAdd: true, + SyncableId: channel1.Id, + Type: model.GroupSyncableTypeChannel, + GroupId: g.Id, + }) + require.Nil(t, err) + } + + // Create Channel2 + channel2 := &model.Channel{ + TeamId: team1.Id, + DisplayName: "Channel2", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + } + channel2, err = ss.Channel().Save(channel2, 9999) + require.Nil(t, err) + + // Create Group3 + group3, err := ss.Group().Create(&model.Group{ + Name: model.NewId(), + DisplayName: "group-3", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, + }) + require.Nil(t, err) + + // And associate it to Channel2 + _, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{ + AutoAdd: true, + SyncableId: channel2.Id, + Type: model.GroupSyncableTypeChannel, + GroupId: group3.Id, + }) + require.Nil(t, err) + + // add members + u1 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user1, err := ss.User().Save(u1) + require.Nil(t, err) + + u2 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user2, err := ss.User().Save(u2) + require.Nil(t, err) + + _, err = ss.Group().UpsertMember(group1.Id, user1.Id) + require.Nil(t, err) + + _, err = ss.Group().UpsertMember(group1.Id, user2.Id) + require.Nil(t, err) + + user2.DeleteAt = 1 + _, err = ss.User().Update(user2, true) + require.Nil(t, err) + + group1WithMemberCount := *group1 + group1WithMemberCount.MemberCount = model.NewInt(1) + + group2WithMemberCount := *group2 + group2WithMemberCount.MemberCount = model.NewInt(0) + + group3WithMemberCount := *group3 + group3WithMemberCount.MemberCount = model.NewInt(0) + + group1WSA := &model.GroupWithSchemeAdmin{Group: *group1, SchemeAdmin: model.NewBool(false)} + group2WSA := &model.GroupWithSchemeAdmin{Group: *group2, SchemeAdmin: model.NewBool(false)} + group3WSA := &model.GroupWithSchemeAdmin{Group: *group3, SchemeAdmin: model.NewBool(false)} + + testCases := []struct { + Name string + TeamId string + Page int + PerPage int + Result map[string][]*model.GroupWithSchemeAdmin + Opts model.GroupSearchOpts + }{ + { + Name: "Get the groups for Channel1 and Channel2", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{}, + Page: 0, + PerPage: 60, + Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA, group2WSA}, channel2.Id: {group3WSA}}, + }, + { + Name: "Get first Group for Channel1 with page 0 with 1 element", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{}, + Page: 0, + PerPage: 1, + Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}}, + }, + { + Name: "Get second Group for Channel1 with page 1 with 1 element", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{}, + Page: 1, + PerPage: 1, + Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group2WSA}}, + }, + { + Name: "Get empty Groups for a fake id", + TeamId: model.NewId(), + Opts: model.GroupSearchOpts{}, + Page: 0, + PerPage: 60, + Result: map[string][]*model.GroupWithSchemeAdmin{}, + }, + { + Name: "Get group matching name", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{Q: string([]rune(group1.Name)[2:10])}, // very low chance of a name collision + Page: 0, + PerPage: 100, + Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}}, + }, + { + Name: "Get group matching display name", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{Q: "rouP-1"}, + Page: 0, + PerPage: 100, + Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}}, + }, + { + Name: "Get group matching multiple display names", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{Q: "roUp-"}, + Page: 0, + PerPage: 100, + Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA, group2WSA}, channel2.Id: {group3WSA}}, + }, + { + Name: "Include member counts", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{IncludeMemberCount: true}, + Page: 0, + PerPage: 10, + Result: map[string][]*model.GroupWithSchemeAdmin{ + channel1.Id: { + {Group: group1WithMemberCount, SchemeAdmin: model.NewBool(false)}, + {Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)}, + }, + channel2.Id: { + {Group: group3WithMemberCount, SchemeAdmin: model.NewBool(false)}, + }, + }, + }, + { + Name: "Include allow reference", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{FilterAllowReference: true}, + Page: 0, + PerPage: 2, + Result: map[string][]*model.GroupWithSchemeAdmin{ + channel1.Id: { + group2WSA, + }, + channel2.Id: { + group3WSA, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + if tc.Opts.PageOpts == nil { + tc.Opts.PageOpts = &model.PageOpts{} + } + tc.Opts.PageOpts.Page = tc.Page + tc.Opts.PageOpts.PerPage = tc.PerPage + groups, err := ss.Group().GetGroupsAssociatedToChannelsByTeam(tc.TeamId, tc.Opts) + require.Nil(t, err) + assert.Equal(t, tc.Result, groups) + }) + } +} + func testGetGroupsByTeam(t *testing.T, ss store.Store) { // Create Team1 team1 := &model.Team{ @@ -2281,27 +2537,30 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) { // Create Groups 1, 2 and a deleted group group1, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-1", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId(), + DisplayName: "group-1", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: false, }) require.Nil(t, err) group2, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-2", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId(), + DisplayName: "group-2", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, }) require.Nil(t, err) deletedGroup, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-deleted", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, - DeleteAt: 1, + Name: model.NewId(), + DisplayName: "group-deleted", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, + DeleteAt: 1, }) require.Nil(t, err) @@ -2332,10 +2591,11 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) { // Create Group3 group3, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-3", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId(), + DisplayName: "group-3", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, }) require.Nil(t, err) @@ -2476,6 +2736,14 @@ func testGetGroupsByTeam(t *testing.T, ss store.Store) { {Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)}, }, }, + { + Name: "Include allow reference", + TeamId: team1.Id, + Opts: model.GroupSearchOpts{FilterAllowReference: true}, + Page: 0, + PerPage: 100, + Result: []*model.GroupWithSchemeAdmin{group2WSA}, + }, } for _, tc := range testCases { @@ -2525,27 +2793,30 @@ func testGetGroups(t *testing.T, ss store.Store) { // Create Groups 1 and 2 group1, err := ss.Group().Create(&model.Group{ - Name: model.NewId(), - DisplayName: "group-1", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId(), + DisplayName: "group-1", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, }) require.Nil(t, err) group2, err := ss.Group().Create(&model.Group{ - Name: model.NewId() + "-group-2", - DisplayName: "group-2", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId() + "-group-2", + DisplayName: "group-2", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: false, }) require.Nil(t, err) deletedGroup, err := ss.Group().Create(&model.Group{ - Name: model.NewId() + "-group-deleted", - DisplayName: "group-deleted", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, - DeleteAt: 1, + Name: model.NewId() + "-group-deleted", + DisplayName: "group-deleted", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: false, + DeleteAt: 1, }) require.Nil(t, err) @@ -2586,10 +2857,11 @@ func testGetGroups(t *testing.T, ss store.Store) { // Create Group3 group3, err := ss.Group().Create(&model.Group{ - Name: model.NewId() + "-group-3", - DisplayName: "group-3", - RemoteId: model.NewId(), - Source: model.GroupSourceLdap, + Name: model.NewId() + "-group-3", + DisplayName: "group-3", + RemoteId: model.NewId(), + Source: model.GroupSourceLdap, + AllowReference: true, }) require.Nil(t, err) @@ -2788,6 +3060,26 @@ func testGetGroups(t *testing.T, ss store.Store) { return true }, }, + { + Name: "Include allow reference", + Opts: model.GroupSearchOpts{FilterAllowReference: true}, + Page: 0, + PerPage: 100, + Resultf: func(groups []*model.Group) bool { + if len(groups) == 0 { + return false + } + for _, g := range groups { + if !g.AllowReference { + return false + } + if g.DeleteAt != 0 { + return false + } + } + return true + }, + }, } for _, tc := range testCases { diff --git a/store/storetest/mocks/GroupStore.go b/store/storetest/mocks/GroupStore.go index ab594635c3..cf9d58a696 100644 --- a/store/storetest/mocks/GroupStore.go +++ b/store/storetest/mocks/GroupStore.go @@ -604,6 +604,31 @@ func (_m *GroupStore) GetGroupsByChannel(channelId string, opts model.GroupSearc return r0, r1 } +// GetGroupsAssociatedToChannelsByTeam provides a mock function with given fields: teamId, opts +func (_m *GroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) { + ret := _m.Called(teamId, opts) + + var r0 map[string][]*model.GroupWithSchemeAdmin + if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) map[string][]*model.GroupWithSchemeAdmin); ok { + r0 = rf(teamId, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]*model.GroupWithSchemeAdmin) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) *model.AppError); ok { + r1 = rf(teamId, opts) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetGroupsByTeam provides a mock function with given fields: teamId, opts func (_m *GroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, *model.AppError) { ret := _m.Called(teamId, opts) diff --git a/store/timer_layer.go b/store/timer_layer.go index e50ddd3fdd..f5a51a3416 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -2990,6 +2990,22 @@ func (s *TimerLayerGroupStore) GetGroupsByChannel(channelId string, opts model.G return resultVar0, resultVar1 } +func (s *TimerLayerGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) { + start := timemodule.Now() + + resultVar0, resultVar1 := s.GroupStore.GetGroupsAssociatedToChannelsByTeam(teamId, opts) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if resultVar1 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupsAssociatedToChannelsByTeam", success, elapsed) + } + return resultVar0, resultVar1 +} + func (s *TimerLayerGroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, *model.AppError) { start := timemodule.Now() diff --git a/web/params.go b/web/params.go index 6c8d9b18fa..9f62382073 100644 --- a/web/params.go +++ b/web/params.go @@ -75,6 +75,7 @@ type Params struct { GroupIDs string IncludeTotalCount bool IncludeDeleted bool + FilterAllowReference bool } func ParamsFromRequest(r *http.Request) *Params { @@ -277,6 +278,10 @@ func ParamsFromRequest(r *http.Request) *Params { params.NotAssociatedToTeam = query.Get("not_associated_to_team") params.NotAssociatedToChannel = query.Get("not_associated_to_channel") + if val, err := strconv.ParseBool(query.Get("filter_allow_reference")); err == nil { + params.FilterAllowReference = val + } + if val, err := strconv.ParseBool(query.Get("paginate")); err == nil { params.Paginate = &val }