diff --git a/api4/role.go b/api4/role.go index 9114a4c65b..26ea67757b 100644 --- a/api4/role.go +++ b/api4/role.go @@ -5,7 +5,6 @@ package api4 import ( "net/http" - "strings" "github.com/mattermost/mattermost-server/v5/audit" "github.com/mattermost/mattermost-server/v5/model" @@ -56,18 +55,10 @@ func getRolesByNames(c *Context, w http.ResponseWriter, r *http.Request) { return } - var cleanedRoleNames []string - for _, rolename := range rolenames { - if strings.TrimSpace(rolename) == "" { - continue - } - - if !model.IsValidRoleName(rolename) { - c.SetInvalidParam("rolename") - return - } - - cleanedRoleNames = append(cleanedRoleNames, rolename) + cleanedRoleNames, valid := model.CleanRoleNames(rolenames) + if !valid { + c.SetInvalidParam("rolename") + return } roles, err := c.App.GetRolesByNames(cleanedRoleNames) diff --git a/api4/user.go b/api4/user.go index 761e8e0e77..78099497a5 100644 --- a/api4/user.go +++ b/api4/user.go @@ -30,6 +30,7 @@ func (api *API) InitUser() { api.BaseRoutes.Users.Handle("/search", api.ApiSessionRequiredDisableWhenBusy(searchUsers)).Methods("POST") api.BaseRoutes.Users.Handle("/autocomplete", api.ApiSessionRequired(autocompleteUsers)).Methods("GET") api.BaseRoutes.Users.Handle("/stats", api.ApiSessionRequired(getTotalUsersStats)).Methods("GET") + api.BaseRoutes.Users.Handle("/stats/filtered", api.ApiSessionRequired(getFilteredUsersStats)).Methods("GET") api.BaseRoutes.Users.Handle("/group_channels", api.ApiSessionRequired(getUsersByGroupChannelIds)).Methods("POST") api.BaseRoutes.User.Handle("", api.ApiSessionRequired(getUser)).Methods("GET") @@ -512,6 +513,68 @@ func getTotalUsersStats(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(stats.ToJson())) } +func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) { + teamID := r.URL.Query().Get("in_team") + channelID := r.URL.Query().Get("in_channel") + includeDeleted := r.URL.Query().Get("include_deleted") + includeBotAccounts := r.URL.Query().Get("include_bots") + rolesString := r.URL.Query().Get("roles") + channelRolesString := r.URL.Query().Get("channel_roles") + teamRolesString := r.URL.Query().Get("team_roles") + + includeDeletedBool, _ := strconv.ParseBool(includeDeleted) + includeBotAccountsBool, _ := strconv.ParseBool(includeBotAccounts) + + roles := []string{} + var rolesValid bool + if rolesString != "" { + roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ",")) + if !rolesValid { + c.SetInvalidParam("roles") + return + } + } + channelRoles := []string{} + if channelRolesString != "" && len(channelID) != 0 { + channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ",")) + if !rolesValid { + c.SetInvalidParam("channelRoles") + return + } + } + teamRoles := []string{} + if teamRolesString != "" && len(teamID) != 0 { + teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ",")) + if !rolesValid { + c.SetInvalidParam("teamRoles") + return + } + } + + options := &model.UserCountOptions{ + IncludeDeleted: includeDeletedBool, + IncludeBotAccounts: includeBotAccountsBool, + TeamId: teamID, + ChannelId: channelID, + Roles: roles, + ChannelRoles: channelRoles, + TeamRoles: teamRoles, + } + + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + stats, err := c.App.GetFilteredUsersStats(options) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(stats.ToJson())) +} + func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) { channelIds := model.ArrayFromJson(r.Body) @@ -542,6 +605,9 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { active := r.URL.Query().Get("active") role := r.URL.Query().Get("role") sort := r.URL.Query().Get("sort") + rolesString := r.URL.Query().Get("roles") + channelRolesString := r.URL.Query().Get("channel_roles") + teamRolesString := r.URL.Query().Get("team_roles") if len(notInChannelId) > 0 && len(inTeamId) == 0 { c.SetInvalidUrlParam("team_id") @@ -573,6 +639,32 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetInvalidUrlParam("inactive") } + roles := []string{} + var rolesValid bool + if rolesString != "" { + roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ",")) + if !rolesValid { + c.SetInvalidParam("roles") + return + } + } + channelRoles := []string{} + if channelRolesString != "" && len(inChannelId) != 0 { + channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ",")) + if !rolesValid { + c.SetInvalidParam("channelRoles") + return + } + } + teamRoles := []string{} + if teamRolesString != "" && len(inTeamId) != 0 { + teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ",")) + if !rolesValid { + c.SetInvalidParam("teamRoles") + return + } + } + restrictions, err := c.App.GetViewUsersRestrictions(c.App.Session().UserId) if err != nil { c.Err = err @@ -590,6 +682,9 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { Inactive: inactiveBool, Active: activeBool, Role: role, + Roles: roles, + ChannelRoles: channelRoles, + TeamRoles: teamRoles, Sort: sort, Page: c.Params.Page, PerPage: c.Params.PerPage, @@ -824,6 +919,9 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { GroupConstrained: props.GroupConstrained, Limit: props.Limit, Role: props.Role, + Roles: props.Roles, + ChannelRoles: props.ChannelRoles, + TeamRoles: props.TeamRoles, } if c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { diff --git a/app/app_iface.go b/app/app_iface.go index d1b92bddd1..0b80900905 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -157,6 +157,8 @@ type AppIface interface { GetEmojiStaticUrl(emojiName string) (string, *model.AppError) // GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable. GetEnvironmentConfig() map[string]interface{} + // GetFilteredUsersStats is used to get a count of users based on the set of filters supported by UserCountOptions. + GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) // 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) // GetKnownUsers returns the list of user ids of users with any direct diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index f72a9dfbf5..7033526f3d 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -5158,6 +5158,28 @@ func (a *OpenTracingAppLayer) GetFileInfosForPostWithMigration(postId string) ([ return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFilteredUsersStats") + + 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.GetFilteredUsersStats(options) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetFlaggedPosts(userId string, offset int, limit int) (*model.PostList, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFlaggedPosts") diff --git a/app/user.go b/app/user.go index 9b50bb32f1..41b1d2d197 100644 --- a/app/user.go +++ b/app/user.go @@ -1649,6 +1649,18 @@ func (a *App) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) return stats, nil } +// GetFilteredUsersStats is used to get a count of users based on the set of filters supported by UserCountOptions. +func (a *App) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) { + count, err := a.Srv().Store.User().Count(*options) + if err != nil { + return nil, err + } + stats := &model.UsersStats{ + TotalUsersCount: count, + } + return stats, nil +} + func (a *App) VerifyUserEmail(userId, email string) *model.AppError { if _, err := a.Srv().Store.User().VerifyEmail(userId, email); err != nil { return err diff --git a/model/role.go b/model/role.go index 38ac1ef7ef..837db366c8 100644 --- a/model/role.go +++ b/model/role.go @@ -364,6 +364,23 @@ func (r *Role) IsValidWithoutId() bool { return true } +func CleanRoleNames(roleNames []string) ([]string, bool) { + var cleanedRoleNames []string + for _, roleName := range roleNames { + if strings.TrimSpace(roleName) == "" { + continue + } + + if !IsValidRoleName(roleName) { + return roleNames, false + } + + cleanedRoleNames = append(cleanedRoleNames, roleName) + } + + return cleanedRoleNames, true +} + func IsValidRoleName(roleName string) bool { if len(roleName) <= 0 || len(roleName) > ROLE_NAME_MAX_LENGTH { return false diff --git a/model/user_count.go b/model/user_count.go index 3c20b23a09..ee474883a5 100644 --- a/model/user_count.go +++ b/model/user_count.go @@ -13,6 +13,14 @@ type UserCountOptions struct { ExcludeRegularUsers bool // Only include users on a specific team. "" for any team. TeamId string + // Only include users on a specific channel. "" for any channel. + ChannelId string // Restrict to search in a list of teams and channels ViewRestrictions *ViewUsersRestrictions + // Only include users matching any of the given system wide roles. + Roles []string + // Only include users matching any of the given channel roles, must be used with ChannelId. + ChannelRoles []string + // Only include users matching any of the given team roles, must be used with TeamId. + TeamRoles []string } diff --git a/model/user_get.go b/model/user_get.go index 0d975e3319..2748d73513 100644 --- a/model/user_get.go +++ b/model/user_get.go @@ -24,6 +24,12 @@ type UserGetOptions struct { Active bool // Filters for the given role Role string + // Filters for users matching any of the given system wide roles + Roles []string + // Filters for users matching any of the given channel roles, must be used with InChannelId + ChannelRoles []string + // Filters for users matching any of the given team roles, must be used with InTeamId + TeamRoles []string // Sorting option Sort string // Restrict to search in a list of teams and channels diff --git a/model/user_search.go b/model/user_search.go index 7ae8a33103..0a721eacb0 100644 --- a/model/user_search.go +++ b/model/user_search.go @@ -13,17 +13,20 @@ const USER_SEARCH_DEFAULT_LIMIT = 100 // UserSearch captures the parameters provided by a client for initiating a user search. type UserSearch struct { - Term string `json:"term"` - TeamId string `json:"team_id"` - NotInTeamId string `json:"not_in_team_id"` - InChannelId string `json:"in_channel_id"` - NotInChannelId string `json:"not_in_channel_id"` - InGroupId string `json:"in_group_id"` - GroupConstrained bool `json:"group_constrained"` - AllowInactive bool `json:"allow_inactive"` - WithoutTeam bool `json:"without_team"` - Limit int `json:"limit"` - Role string `json:"role"` + Term string `json:"term"` + TeamId string `json:"team_id"` + NotInTeamId string `json:"not_in_team_id"` + InChannelId string `json:"in_channel_id"` + NotInChannelId string `json:"not_in_channel_id"` + InGroupId string `json:"in_group_id"` + GroupConstrained bool `json:"group_constrained"` + AllowInactive bool `json:"allow_inactive"` + WithoutTeam bool `json:"without_team"` + Limit int `json:"limit"` + Role string `json:"role"` + Roles []string `json:"roles"` + ChannelRoles []string `json:"channel_roles"` + TeamRoles []string `json:"team_roles"` } // ToJson convert a User to a json string @@ -61,6 +64,12 @@ type UserSearchOptions struct { Limit int // Filters for the given role Role string + // Filters for users that have any of the given system roles + Roles []string + // Filters for users that have the given channel roles to be used when searching in a channel + ChannelRoles []string + // Filters for users that have the given team roles to be used when searching in a team + TeamRoles []string // Restrict to search in a list of teams and channels ViewRestrictions *ViewUsersRestrictions // List of allowed channels diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index eb297fa568..56a0e6edd0 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -458,6 +458,121 @@ func applyRoleFilter(query sq.SelectBuilder, role string, isPostgreSQL bool) sq. return query.Where("u.Roles LIKE ? ESCAPE '*'", roleParam) } +func applyMultiRoleFilters(query sq.SelectBuilder, roles []string, teamRoles []string, channelRoles []string) sq.SelectBuilder { + queryString := "" + if len(roles) > 0 && roles[0] != "" { + schemeGuest := false + schemeAdmin := false + schemeUser := false + + for _, role := range roles { + switch role { + case model.SYSTEM_ADMIN_ROLE_ID: + schemeAdmin = true + case model.SYSTEM_USER_ROLE_ID: + schemeUser = true + case model.SYSTEM_GUEST_ROLE_ID: + schemeGuest = true + } + } + + if schemeAdmin || schemeUser || schemeGuest { + if schemeAdmin && schemeUser { + queryString += `(u.Roles LIKE '%system_user%' OR u.Roles LIKE '%system_admin%') ` + } else if schemeAdmin { + queryString += `(u.Roles LIKE '%system_admin%') ` + } else if schemeUser { + queryString += `(u.Roles LIKE '%system_user%' AND u.Roles NOT LIKE '%system_admin%') ` + } + + if schemeGuest { + if queryString != "" { + queryString += "OR " + } + queryString += `(u.Roles LIKE '%system_guest%') ` + } + } + } + + if len(channelRoles) > 0 && channelRoles[0] != "" { + schemeGuest := false + schemeAdmin := false + schemeUser := false + for _, channelRole := range channelRoles { + switch channelRole { + case model.CHANNEL_ADMIN_ROLE_ID: + schemeAdmin = true + case model.CHANNEL_USER_ROLE_ID: + schemeUser = true + case model.CHANNEL_GUEST_ROLE_ID: + schemeGuest = true + } + } + + if schemeAdmin || schemeUser || schemeGuest { + if queryString != "" { + queryString += "OR " + } + if schemeAdmin && schemeUser { + queryString += `(cm.SchemeUser = true AND u.Roles = 'system_user')` + } else if schemeAdmin { + queryString += `(cm.SchemeAdmin = true AND u.Roles = 'system_user')` + } else if schemeUser { + queryString += `(cm.SchemeUser = true AND cm.SchemeAdmin = false AND u.Roles = 'system_user')` + } + + if schemeGuest { + if queryString != "" && queryString[len(queryString)-3:] != "OR " { + queryString += "OR " + } + queryString += `(cm.SchemeGuest = true AND u.Roles = 'system_guest')` + } + } + } + + if len(teamRoles) > 0 && teamRoles[0] != "" { + schemeAdmin := false + schemeUser := false + schemeGuest := false + for _, teamRole := range teamRoles { + switch teamRole { + case model.TEAM_ADMIN_ROLE_ID: + schemeAdmin = true + case model.TEAM_USER_ROLE_ID: + schemeUser = true + case model.TEAM_GUEST_ROLE_ID: + schemeGuest = true + } + } + + if schemeAdmin || schemeUser || schemeGuest { + if queryString != "" { + queryString += "OR " + } + if schemeAdmin && schemeUser { + queryString += `(tm.SchemeUser = true AND u.Roles = 'system_user')` + } else if schemeAdmin { + queryString += `(tm.SchemeAdmin = true AND u.Roles = 'system_user')` + } else if schemeUser { + queryString += `(tm.SchemeUser = true AND tm.SchemeAdmin = false AND u.Roles = 'system_user')` + } + + if schemeGuest { + if queryString != "" && queryString[len(queryString)-3:] != "OR " { + queryString += "OR " + } + queryString += `(tm.SchemeGuest = true AND u.Roles = 'system_guest')` + } + } + } + + if queryString != "" { + query = query.Where("(" + queryString + ")") + } + + return query +} + func applyChannelGroupConstrainedFilter(query sq.SelectBuilder, channelId string) sq.SelectBuilder { if channelId == "" { return query @@ -525,6 +640,7 @@ func (us SqlUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true) query = applyRoleFilter(query, options.Role, isPostgreSQL) + query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles) if options.Inactive { query = query.Where("u.DeleteAt != 0") @@ -1079,6 +1195,7 @@ func (us SqlUserStore) PermanentDelete(userId string) *model.AppError { } func (us SqlUserStore) Count(options model.UserCountOptions) (int64, *model.AppError) { + isPostgreSQL := us.DriverName() == model.DATABASE_DRIVER_POSTGRES query := us.getQueryBuilder().Select("COUNT(DISTINCT u.Id)").From("Users AS u") if !options.IncludeDeleted { @@ -1099,10 +1216,13 @@ func (us SqlUserStore) Count(options model.UserCountOptions) (int64, *model.AppE if options.TeamId != "" { query = query.LeftJoin("TeamMembers AS tm ON u.Id = tm.UserId").Where("tm.TeamId = ? AND tm.DeleteAt = 0", options.TeamId) + } else if options.ChannelId != "" { + query = query.LeftJoin("ChannelMembers AS cm ON u.Id = cm.UserId").Where("cm.ChannelId = ?", options.ChannelId) } query = applyViewRestrictionsFilter(query, options.ViewRestrictions, false) + query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles) - if us.DriverName() == model.DATABASE_DRIVER_POSTGRES { + if isPostgreSQL { query = query.PlaceholderFormat(sq.Dollar) } @@ -1309,6 +1429,7 @@ func (us SqlUserStore) performSearch(query sq.SelectBuilder, term string, option isPostgreSQL := us.DriverName() == model.DATABASE_DRIVER_POSTGRES query = applyRoleFilter(query, options.Role, isPostgreSQL) + query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles) if !options.AllowInactive { query = query.Where("u.DeleteAt = 0") diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index e25927feb9..10a8c15658 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -68,6 +68,7 @@ func TestUserStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("UpdateMfaActive", func(t *testing.T) { testUserStoreUpdateMfaActive(t, ss) }) t.Run("GetRecentlyActiveUsersForTeam", func(t *testing.T) { testUserStoreGetRecentlyActiveUsersForTeam(t, ss, s) }) t.Run("GetNewUsersForTeam", func(t *testing.T) { testUserStoreGetNewUsersForTeam(t, ss) }) + t.Run("Search", func(t *testing.T) { testUserStoreSearch(t, ss) }) t.Run("SearchNotInChannel", func(t *testing.T) { testUserStoreSearchNotInChannel(t, ss) }) t.Run("SearchInChannel", func(t *testing.T) { testUserStoreSearchInChannel(t, ss) }) t.Run("SearchNotInTeam", func(t *testing.T) { testUserStoreSearchNotInTeam(t, ss) }) @@ -2291,6 +2292,153 @@ func assertUsers(t *testing.T, expected, actual []*model.User) { } } +func testUserStoreSearch(t *testing.T, ss store.Store) { + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", + Roles: "system_user system_admin", + } + _, err := ss.User().Save(u1) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(u1.Id)) }() + + u2 := &model.User{ + Username: "jim2-bobby" + model.NewId(), + Email: MakeEmail(), + Roles: "system_user", + } + _, err = ss.User().Save(u2) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(u2.Id)) }() + + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: MakeEmail(), + Roles: "system_guest", + } + _, err = ss.User().Save(u3) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(u3.Id)) }() + + // The users returned from the database will have AuthData as an empty string. + nilAuthData := new(string) + *nilAuthData = "" + u1.AuthData = nilAuthData + u2.AuthData = nilAuthData + u3.AuthData = nilAuthData + + t1id := model.NewId() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u1.Id, SchemeAdmin: true, SchemeUser: true}, -1) + require.Nil(t, err) + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u2.Id, SchemeAdmin: true, SchemeUser: true}, -1) + require.Nil(t, err) + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u3.Id, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true}, -1) + require.Nil(t, err) + + testCases := []struct { + Description string + TeamId string + Term string + Options *model.UserSearchOptions + Expected []*model.User + }{ + { + "search jimb, team 1", + t1id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1, u3}, + }, + { + "search jimb, team 1 with team guest and team admin filters without sys admin filter", + t1id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + TeamRoles: []string{model.TEAM_GUEST_ROLE_ID, model.TEAM_ADMIN_ROLE_ID}, + }, + []*model.User{u3}, + }, + { + "search jimb, team 1 with team admin filter and sys admin filter", + t1id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + TeamRoles: []string{model.TEAM_ADMIN_ROLE_ID}, + }, + []*model.User{u1}, + }, + { + "search jim, team 1 with team admin filter", + t1id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + TeamRoles: []string{model.TEAM_ADMIN_ROLE_ID}, + }, + []*model.User{u2}, + }, + { + "search jim, team 1 with team admin and team guest filter", + t1id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + TeamRoles: []string{model.TEAM_ADMIN_ROLE_ID, model.TEAM_GUEST_ROLE_ID}, + }, + []*model.User{u2, u3}, + }, + { + "search jim, team 1 with team admin and system admin filters", + t1id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + TeamRoles: []string{model.TEAM_ADMIN_ROLE_ID}, + }, + []*model.User{u2, u1}, + }, + { + "search jim, team 1 with system guest filter", + t1id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Roles: []string{model.SYSTEM_GUEST_ROLE_ID}, + TeamRoles: []string{}, + }, + []*model.User{u3}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + users, err := ss.User().Search( + testCase.TeamId, + testCase.Term, + testCase.Options, + ) + require.Nil(t, err) + assertUsers(t, testCase.Expected, users) + }) + } +} + func testUserStoreSearchNotInChannel(t *testing.T, ss store.Store) { u1 := &model.User{ Username: "jimbo1" + model.NewId(), @@ -2524,6 +2672,7 @@ func testUserStoreSearchInChannel(t *testing.T, ss store.Store) { LastName: "Bill", Nickname: "Rob", Email: "harold" + model.NewId() + "@simulator.amazonses.com", + Roles: "system_user system_admin", } _, err := ss.User().Save(u1) require.Nil(t, err) @@ -2532,6 +2681,7 @@ func testUserStoreSearchInChannel(t *testing.T, ss store.Store) { u2 := &model.User{ Username: "jim-bobby" + model.NewId(), Email: MakeEmail(), + Roles: "system_user", } _, err = ss.User().Save(u2) require.Nil(t, err) @@ -2541,6 +2691,7 @@ func testUserStoreSearchInChannel(t *testing.T, ss store.Store) { Username: "jimbo3" + model.NewId(), Email: MakeEmail(), DeleteAt: 1, + Roles: "system_user", } _, err = ss.User().Save(u3) require.Nil(t, err) @@ -2592,18 +2743,24 @@ func testUserStoreSearchInChannel(t *testing.T, ss store.Store) { ChannelId: c1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeAdmin: true, + SchemeUser: true, }) require.Nil(t, err) _, err = ss.Channel().SaveMember(&model.ChannelMember{ ChannelId: c2.Id, UserId: u2.Id, NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeAdmin: false, + SchemeUser: true, }) require.Nil(t, err) _, err = ss.Channel().SaveMember(&model.ChannelMember{ ChannelId: c1.Id, UserId: u3.Id, NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeAdmin: false, + SchemeUser: true, }) require.Nil(t, err) @@ -2667,6 +2824,66 @@ func testUserStoreSearchInChannel(t *testing.T, ss store.Store) { }, []*model.User{}, }, + { + "search jim, allow inactive, channel 1 with system admin filter", + c1.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + }, + []*model.User{u1}, + }, + { + "search jim, allow inactive, channel 1 with system admin and system user filter", + c1.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID, model.SYSTEM_USER_ROLE_ID}, + }, + []*model.User{u1, u3}, + }, + { + "search jim, allow inactive, channel 1 with channel user filter", + c1.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + ChannelRoles: []string{model.CHANNEL_USER_ROLE_ID}, + }, + []*model.User{u3}, + }, + { + "search jim, allow inactive, channel 1 with channel user and channel admin filter", + c1.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + ChannelRoles: []string{model.CHANNEL_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID}, + }, + []*model.User{u3}, + }, + { + "search jim, allow inactive, channel 2 with channel user filter", + c2.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + ChannelRoles: []string{model.CHANNEL_USER_ROLE_ID}, + }, + []*model.User{u2}, + }, } for _, testCase := range testCases { @@ -3122,111 +3339,287 @@ func testUserStoreSearchInGroup(t *testing.T, ss store.Store) { func testCount(t *testing.T, ss store.Store) { // Regular teamId := model.NewId() - u1 := &model.User{} - u1.Email = MakeEmail() - _, err := ss.User().Save(u1) + channelId := model.NewId() + regularUser := &model.User{} + regularUser.Email = MakeEmail() + regularUser.Roles = model.SYSTEM_USER_ROLE_ID + _, err := ss.User().Save(regularUser) require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u1.Id)) }() - _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1) + defer func() { require.Nil(t, ss.User().PermanentDelete(regularUser.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: regularUser.Id, SchemeAdmin: false, SchemeUser: true}, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{UserId: regularUser.Id, ChannelId: channelId, SchemeAdmin: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + guestUser := &model.User{} + guestUser.Email = MakeEmail() + guestUser.Roles = model.SYSTEM_GUEST_ROLE_ID + _, err = ss.User().Save(guestUser) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(guestUser.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: guestUser.Id, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true}, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{UserId: guestUser.Id, ChannelId: channelId, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + teamAdmin := &model.User{} + teamAdmin.Email = MakeEmail() + teamAdmin.Roles = model.SYSTEM_USER_ROLE_ID + _, err = ss.User().Save(teamAdmin) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(teamAdmin.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: teamAdmin.Id, SchemeAdmin: true, SchemeUser: true}, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{UserId: teamAdmin.Id, ChannelId: channelId, SchemeAdmin: true, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + sysAdmin := &model.User{} + sysAdmin.Email = MakeEmail() + sysAdmin.Roles = model.SYSTEM_ADMIN_ROLE_ID + " " + model.SYSTEM_USER_ROLE_ID + _, err = ss.User().Save(sysAdmin) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(sysAdmin.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: sysAdmin.Id, SchemeAdmin: false, SchemeUser: true}, -1) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{UserId: sysAdmin.Id, ChannelId: channelId, SchemeAdmin: true, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()}) require.Nil(t, err) // Deleted - u2 := &model.User{} - u2.Email = MakeEmail() - u2.DeleteAt = model.GetMillis() - _, err = ss.User().Save(u2) + deletedUser := &model.User{} + deletedUser.Email = MakeEmail() + deletedUser.DeleteAt = model.GetMillis() + _, err = ss.User().Save(deletedUser) require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u2.Id)) }() + defer func() { require.Nil(t, ss.User().PermanentDelete(deletedUser.Id)) }() // Bot - u3, err := ss.User().Save(&model.User{ + botUser, err := ss.User().Save(&model.User{ Email: MakeEmail(), }) require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u3.Id)) }() + defer func() { require.Nil(t, ss.User().PermanentDelete(botUser.Id)) }() _, nErr := ss.Bot().Save(&model.Bot{ - UserId: u3.Id, - Username: u3.Username, - OwnerId: u1.Id, + UserId: botUser.Id, + Username: botUser.Username, + OwnerId: regularUser.Id, }) require.Nil(t, nErr) - u3.IsBot = true - defer func() { require.Nil(t, ss.Bot().PermanentDelete(u3.Id)) }() + botUser.IsBot = true + defer func() { require.Nil(t, ss.Bot().PermanentDelete(botUser.Id)) }() - count, err := ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: false, - IncludeDeleted: false, - TeamId: "", - }) - require.Nil(t, err) - require.Equal(t, int64(1), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: true, - IncludeDeleted: false, - TeamId: "", - }) - require.Nil(t, err) - require.Equal(t, int64(2), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: false, - IncludeDeleted: true, - TeamId: "", - }) - require.Nil(t, err) - require.Equal(t, int64(2), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: true, - IncludeDeleted: true, - TeamId: "", - }) - require.Nil(t, err) - require.Equal(t, int64(3), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: true, - IncludeDeleted: true, - ExcludeRegularUsers: true, - TeamId: "", - }) - require.Nil(t, err) - require.Equal(t, int64(1), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: true, - IncludeDeleted: true, - TeamId: teamId, - }) - require.Nil(t, err) - require.Equal(t, int64(1), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: true, - IncludeDeleted: true, - TeamId: model.NewId(), - }) - require.Nil(t, err) - require.Equal(t, int64(0), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: true, - IncludeDeleted: true, - TeamId: teamId, - ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{teamId}}, - }) - require.Nil(t, err) - require.Equal(t, int64(1), count) - - count, err = ss.User().Count(model.UserCountOptions{ - IncludeBotAccounts: true, - IncludeDeleted: true, - TeamId: teamId, - ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{model.NewId()}}, - }) - require.Nil(t, err) - require.Equal(t, int64(0), count) + testCases := []struct { + Description string + Options model.UserCountOptions + Expected int64 + }{ + { + "No bot accounts no deleted accounts and no team id", + model.UserCountOptions{ + IncludeBotAccounts: false, + IncludeDeleted: false, + TeamId: "", + }, + 4, + }, + { + "Include bot accounts no deleted accounts and no team id", + model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: false, + TeamId: "", + }, + 5, + }, + { + "Include delete accounts no bots and no team id", + model.UserCountOptions{ + IncludeBotAccounts: false, + IncludeDeleted: true, + TeamId: "", + }, + 5, + }, + { + "Include bot accounts and deleted accounts and no team id", + model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: "", + }, + 6, + }, + { + "Include bot accounts, deleted accounts, exclude regular users with no team id", + model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + ExcludeRegularUsers: true, + TeamId: "", + }, + 1, + }, + { + "Include bot accounts and deleted accounts with existing team id", + model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: teamId, + }, + 4, + }, + { + "Include bot accounts and deleted accounts with fake team id", + model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: model.NewId(), + }, + 0, + }, + { + "Include bot accounts and deleted accounts with existing team id and view restrictions allowing team", + model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: teamId, + ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{teamId}}, + }, + 4, + }, + { + "Include bot accounts and deleted accounts with existing team id and view restrictions not allowing current team", + model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: teamId, + ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{model.NewId()}}, + }, + 0, + }, + { + "Filter by system admins only", + model.UserCountOptions{ + TeamId: teamId, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + }, + 1, + }, + { + "Filter by system users only", + model.UserCountOptions{ + TeamId: teamId, + Roles: []string{model.SYSTEM_USER_ROLE_ID}, + }, + 2, + }, + { + "Filter by system guests only", + model.UserCountOptions{ + TeamId: teamId, + Roles: []string{model.SYSTEM_GUEST_ROLE_ID}, + }, + 1, + }, + { + "Filter by system admins and system users", + model.UserCountOptions{ + TeamId: teamId, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID, model.SYSTEM_USER_ROLE_ID}, + }, + 3, + }, + { + "Filter by system admins, system user and system guests", + model.UserCountOptions{ + TeamId: teamId, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID, model.SYSTEM_USER_ROLE_ID, model.SYSTEM_GUEST_ROLE_ID}, + }, + 4, + }, + { + "Filter by team admins", + model.UserCountOptions{ + TeamId: teamId, + TeamRoles: []string{model.TEAM_ADMIN_ROLE_ID}, + }, + 1, + }, + { + "Filter by team members", + model.UserCountOptions{ + TeamId: teamId, + TeamRoles: []string{model.TEAM_USER_ROLE_ID}, + }, + 1, + }, + { + "Filter by team guests", + model.UserCountOptions{ + TeamId: teamId, + TeamRoles: []string{model.TEAM_GUEST_ROLE_ID}, + }, + 1, + }, + { + "Filter by team guests and any system role", + model.UserCountOptions{ + TeamId: teamId, + TeamRoles: []string{model.TEAM_GUEST_ROLE_ID}, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + }, + 2, + }, + { + "Filter by channel members", + model.UserCountOptions{ + ChannelId: channelId, + ChannelRoles: []string{model.CHANNEL_USER_ROLE_ID}, + }, + 1, + }, + { + "Filter by channel members and system admins", + model.UserCountOptions{ + ChannelId: channelId, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + ChannelRoles: []string{model.CHANNEL_USER_ROLE_ID}, + }, + 2, + }, + { + "Filter by channel members and system admins and channel admins", + model.UserCountOptions{ + ChannelId: channelId, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + ChannelRoles: []string{model.CHANNEL_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID}, + }, + 3, + }, + { + "Filter by channel guests", + model.UserCountOptions{ + ChannelId: channelId, + ChannelRoles: []string{model.CHANNEL_GUEST_ROLE_ID}, + }, + 1, + }, + { + "Filter by channel guests and any system role", + model.UserCountOptions{ + ChannelId: channelId, + ChannelRoles: []string{model.CHANNEL_GUEST_ROLE_ID}, + Roles: []string{model.SYSTEM_ADMIN_ROLE_ID}, + }, + 2, + }, + } + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + count, err := ss.User().Count(testCase.Options) + require.Nil(t, err) + require.Equal(t, testCase.Expected, count) + }) + } } func testUserStoreAnalyticsActiveCount(t *testing.T, ss store.Store, s SqlSupplier) {