mattermost-community-enterp.../channels/store/sqlstore/team_store.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

1761 lines
58 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"slices"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
)
const (
TeamMemberExistsError = "store.sql_team.save_member.exists.app_error"
)
type SqlTeamStore struct {
*SqlStore
teamsQuery sq.SelectBuilder
teamMembersQuery sq.SelectBuilder
}
type teamMember struct {
TeamId string
UserId string
Roles string
DeleteAt int64
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
SchemeGuest sql.NullBool
CreateAt int64
}
func NewTeamMemberFromModel(tm *model.TeamMember) *teamMember {
return &teamMember{
TeamId: tm.TeamId,
UserId: tm.UserId,
Roles: tm.ExplicitRoles,
DeleteAt: tm.DeleteAt,
SchemeGuest: sql.NullBool{Valid: true, Bool: tm.SchemeGuest},
SchemeUser: sql.NullBool{Valid: true, Bool: tm.SchemeUser},
SchemeAdmin: sql.NullBool{Valid: true, Bool: tm.SchemeAdmin},
CreateAt: tm.CreateAt,
}
}
type teamMemberWithSchemeRoles struct {
TeamId string
UserId string
Roles string
DeleteAt int64
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
CreateAt int64
}
type teamMemberWithSchemeRolesList []teamMemberWithSchemeRoles
func teamMemberSliceColumns() []string {
return []string{"TeamId", "UserId", "Roles", "DeleteAt", "SchemeUser", "SchemeAdmin", "SchemeGuest", "CreateAt"}
}
func teamMemberToSlice(member *model.TeamMember) []any {
resultSlice := []any{}
resultSlice = append(resultSlice, member.TeamId)
resultSlice = append(resultSlice, member.UserId)
resultSlice = append(resultSlice, member.ExplicitRoles)
resultSlice = append(resultSlice, member.DeleteAt)
resultSlice = append(resultSlice, member.SchemeUser)
resultSlice = append(resultSlice, member.SchemeAdmin)
resultSlice = append(resultSlice, member.SchemeGuest)
resultSlice = append(resultSlice, member.CreateAt)
return resultSlice
}
func wildcardSearchTerm(term string) string {
return strings.ToLower("%" + term + "%")
}
type rolesInfo struct {
roles []string
explicitRoles []string
schemeGuest bool
schemeUser bool
schemeAdmin bool
}
func getTeamRoles(schemeGuest, schemeUser, schemeAdmin bool, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole string, roles []string) rolesInfo {
result := rolesInfo{
roles: []string{},
explicitRoles: []string{},
schemeGuest: schemeGuest,
schemeUser: schemeUser,
schemeAdmin: schemeAdmin,
}
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
for _, role := range roles {
switch role {
case model.TeamGuestRoleId:
result.schemeGuest = true
case model.TeamUserRoleId:
result.schemeUser = true
case model.TeamAdminRoleId:
result.schemeAdmin = true
default:
result.explicitRoles = append(result.explicitRoles, role)
result.roles = append(result.roles, role)
}
}
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if result.schemeGuest {
if defaultTeamGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamGuestRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamGuestRoleId)
}
}
if result.schemeUser {
if defaultTeamUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamUserRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamUserRoleId)
}
}
if result.schemeAdmin {
if defaultTeamAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamAdminRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamAdminRoleId)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range result.roles {
if role == impliedRole {
alreadyThere = true
}
}
if !alreadyThere {
result.roles = append(result.roles, impliedRole)
}
}
return result
}
func (db teamMemberWithSchemeRoles) ToModel() *model.TeamMember {
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
rolesResult := getTeamRoles(schemeGuest, schemeUser, schemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(db.Roles))
tm := &model.TeamMember{
TeamId: db.TeamId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
DeleteAt: db.DeleteAt,
SchemeGuest: rolesResult.schemeGuest,
SchemeUser: rolesResult.schemeUser,
SchemeAdmin: rolesResult.schemeAdmin,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
CreateAt: db.CreateAt,
}
return tm
}
func (db teamMemberWithSchemeRolesList) ToModel() []*model.TeamMember {
tms := make([]*model.TeamMember, 0)
for _, tm := range db {
tms = append(tms, tm.ToModel())
}
return tms
}
func teamSliceColumns() []string {
return []string{
"Teams.Id",
"Teams.CreateAt",
"Teams.UpdateAt",
"Teams.DeleteAt",
"Teams.DisplayName",
"Teams.Name",
"Teams.Description",
"Teams.Email",
"Teams.Type",
"Teams.CompanyName",
"Teams.AllowedDomains",
"Teams.InviteId",
"Teams.AllowOpenInvite",
"Teams.LastTeamIconUpdate",
"Teams.SchemeId",
"Teams.GroupConstrained",
"Teams.CloudLimitsArchived",
}
}
func newSqlTeamStore(sqlStore *SqlStore) store.TeamStore {
s := &SqlTeamStore{
SqlStore: sqlStore,
}
s.teamsQuery = s.getQueryBuilder().
Select(teamSliceColumns()...).
From("Teams")
s.teamMembersQuery = s.getQueryBuilder().
Select(
"TeamMembers.TeamId",
"TeamMembers.UserId",
"TeamMembers.Roles",
"TeamMembers.DeleteAt",
"TeamMembers.SchemeUser",
"TeamMembers.SchemeAdmin",
"TeamMembers.SchemeGuest",
"TeamMembers.CreateAt",
).
From("TeamMembers")
return s
}
// Save adds the team to the database if a team with the same name does not already
// exist in the database. It returns the team added if the operation is successful.
func (s SqlTeamStore) Save(team *model.Team) (*model.Team, error) {
if team.Id != "" {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
team.PreSave()
if err := team.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMaster().NamedExec(`INSERT INTO Teams
(Id, CreateAt, UpdateAt, DeleteAt, DisplayName, Name, Description, Email, Type, CompanyName, AllowedDomains,
InviteId, AllowOpenInvite, LastTeamIconUpdate, SchemeId, GroupConstrained, CloudLimitsArchived)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :DisplayName, :Name, :Description, :Email, :Type, :CompanyName, :AllowedDomains,
:InviteId, :AllowOpenInvite, :LastTeamIconUpdate, :SchemeId, :GroupConstrained, :CloudLimitsArchived)`, team); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "teams_name_key"}) {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
return nil, errors.Wrapf(err, "failed to save Team with id=%s", team.Id)
}
return team, nil
}
// Update updates the details of the team passed as the parameter using the team Id
// if the team exists in the database.
// It returns the updated team if the operation is successful.
func (s SqlTeamStore) Update(team *model.Team) (*model.Team, error) {
team.PreUpdate()
if err := team.IsValid(); err != nil {
return nil, err
}
oldTeam := model.Team{}
query := s.teamsQuery.Where(sq.Eq{"Id": team.Id})
err := s.GetMaster().GetBuilder(&oldTeam, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get Team with id=%s", team.Id)
}
if oldTeam.Id == "" {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
team.CreateAt = oldTeam.CreateAt
team.UpdateAt = model.GetMillis()
res, err := s.GetMaster().NamedExec(`UPDATE Teams
SET CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, DisplayName=:DisplayName, Name=:Name,
Description=:Description, Email=:Email, Type=:Type, CompanyName=:CompanyName, AllowedDomains=:AllowedDomains,
InviteId=:InviteId, AllowOpenInvite=:AllowOpenInvite, LastTeamIconUpdate=:LastTeamIconUpdate,
SchemeId=:SchemeId, GroupConstrained=:GroupConstrained, CloudLimitsArchived=:CloudLimitsArchived
WHERE Id=:Id`, team)
if err != nil {
return nil, errors.Wrapf(err, "failed to update Team with id=%s", team.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "failed to get rows_affected")
}
if count > 1 {
return nil, errors.Wrapf(err, "multiple Teams updated with id=%s", team.Id)
}
return team, nil
}
// Get returns from the database the team that matches the id provided as parameter.
// If the team doesn't exist it returns a model.AppError with a
// http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) Get(id string) (*model.Team, error) {
team := model.Team{}
query := s.teamsQuery.Where(sq.Eq{"Id": id})
if err := s.GetReplica().GetBuilder(&team, query); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", id)
}
return nil, errors.Wrapf(err, "failed to get Team with id=%s", id)
}
if team.Id == "" {
return nil, store.NewErrNotFound("Team", id)
}
return &team, nil
}
func (s SqlTeamStore) GetMany(ids []string) ([]*model.Team, error) {
query := s.teamsQuery.Where(sq.Eq{"Teams.Id": ids})
teams := []*model.Team{}
err := s.GetReplica().SelectBuilder(&teams, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get teams with ids %v", ids)
}
if len(teams) == 0 {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("ids=%v", ids))
}
return teams, nil
}
// GetByInviteId returns from the database the team that matches the inviteId provided as parameter.
// If the parameter provided is empty or if there is no match in the database, it returns a model.AppError
// with a http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) GetByInviteId(inviteId string) (*model.Team, error) {
team := model.Team{}
query, args, err := s.teamsQuery.Where(sq.Eq{"InviteId": inviteId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplica().Get(&team, query, args...)
if err != nil {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("inviteId=%s", inviteId))
}
if inviteId == "" || team.InviteId != inviteId {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("inviteId=%s", inviteId))
}
return &team, nil
}
func (s SqlTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
teams := []*model.Team{}
query := s.teamsQuery.Where(sq.Eq{"InviteId": ""})
err := s.GetReplica().SelectBuilder(&teams, query)
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams with empty InviteID")
}
return teams, nil
}
// GetByName returns from the database the team that matches the name provided as parameter.
// If there is no match in the database, it returns a model.AppError with a
// http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) GetByName(name string) (*model.Team, error) {
team := model.Team{}
query, args, err := s.teamsQuery.Where(sq.Eq{"Name": name}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplica().Get(&team, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to find Team with name=%s", name)
}
return &team, nil
}
func (s SqlTeamStore) GetByNames(names []string) ([]*model.Team, error) {
uniqueNames := utils.RemoveDuplicatesFromStringArray(names)
query, args, err := s.teamsQuery.Where(sq.Eq{"Name": uniqueNames}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
teams := []*model.Team{}
err = s.GetReplica().Select(&teams, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("nameIn=%v", names))
}
return nil, errors.Wrap(err, "failed to find Teams")
}
if len(teams) != len(uniqueNames) {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("nameIn=%v", names))
}
return teams, nil
}
func (s SqlTeamStore) teamSearchQuery(opts *model.TeamSearch, countQuery bool) sq.SelectBuilder {
var selectStr string
if countQuery {
selectStr = "count(*)"
} else {
selectStr = "t.*"
if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectStr += ", RetentionPoliciesTeams.PolicyId as PolicyID"
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("Teams as t")
// Don't order or limit if getting count
if !countQuery {
query = query.OrderBy("t.DisplayName")
if opts.IsPaginated() {
query = query.Limit(uint64(*opts.PerPage)).Offset(uint64(*opts.Page * *opts.PerPage))
}
}
term := opts.Term
if term != "" {
term = sanitizeSearchTerm(term, "\\")
term = wildcardSearchTerm(term)
operatorKeyword := "ILIKE"
query = query.Where(fmt.Sprintf("(Name %[1]s ? OR DisplayName %[1]s ?)", operatorKeyword), term, term)
}
if opts.PolicyID != nil && *opts.PolicyID != "" {
query = query.
InnerJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where(sq.Eq{"RetentionPoliciesTeams.PolicyId": *opts.PolicyID})
} else if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where("RetentionPoliciesTeams.TeamId IS NULL")
} else if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId")
}
var teamFilters sq.Sqlizer
var openInviteFilter sq.Sqlizer
if opts.AllowOpenInvite != nil {
if *opts.AllowOpenInvite {
openInviteFilter = sq.Eq{"AllowOpenInvite": true}
} else {
openInviteFilter = sq.And{
sq.Or{
sq.NotEq{"AllowOpenInvite": true},
sq.Eq{"AllowOpenInvite": nil},
},
sq.Or{
sq.NotEq{"GroupConstrained": true},
sq.Eq{"GroupConstrained": nil},
},
}
}
teamFilters = openInviteFilter
}
var groupConstrainedFilter sq.Sqlizer
if opts.GroupConstrained != nil {
if *opts.GroupConstrained {
groupConstrainedFilter = sq.Eq{"GroupConstrained": true}
} else {
groupConstrainedFilter = sq.Or{
sq.NotEq{"GroupConstrained": true},
sq.Eq{"GroupConstrained": nil},
}
}
if teamFilters == nil {
teamFilters = groupConstrainedFilter
} else {
teamFilters = sq.Or{teamFilters, groupConstrainedFilter}
}
}
if opts.TeamType != nil {
teamTypeFilter := sq.Eq{"Type": *opts.TeamType}
teamFilters = sq.And{teamFilters, teamTypeFilter}
}
query = query.Where(teamFilters)
return query
}
// SearchAll returns from the database a list of teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
teams := []*model.Team{}
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplica().Select(&teams, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
return teams, nil
}
// SearchAllPaged returns a teams list and the total count of teams that matched the search.
func (s SqlTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
teams := []*model.Team{}
var totalCount int64
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplica().Select(&teams, queryString, args...); err != nil {
return nil, 0, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
queryString, args, err = s.teamSearchQuery(opts, true).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
err = s.GetReplica().Get(&totalCount, queryString, args...)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to count Teams with term=%s", opts.Term)
}
return teams, totalCount, nil
}
// SearchOpen returns from the database a list of public teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewPointer("O")
opts.AllowOpenInvite = model.NewPointer(true)
return s.SearchAll(opts)
}
// SearchPrivate returns from the database a list of private teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewPointer("O")
opts.AllowOpenInvite = model.NewPointer(false)
return s.SearchAll(opts)
}
// GetAll returns all teams
func (s SqlTeamStore) GetAll() ([]*model.Team, error) {
teams := []*model.Team{}
query, args, err := s.teamsQuery.OrderBy("DisplayName").ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplica().Select(&teams, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetAllPage returns teams, up to a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
teams := []*model.Team{}
selectString := "Teams.*"
if opts != nil && opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectString += ", RetentionPoliciesTeams.PolicyId as PolicyID"
}
builder := s.getQueryBuilder().
Select(selectString).
From("Teams").
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
if opts != nil {
if (opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained) ||
(opts.IncludePolicyID != nil && *opts.IncludePolicyID) {
builder = builder.LeftJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId")
}
if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
builder = builder.Where("RetentionPoliciesTeams.TeamId IS NULL")
}
if opts.AllowOpenInvite != nil {
builder = builder.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplica().Select(&teams, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetTeamsByUserId returns from the database all teams that userId belongs to.
func (s SqlTeamStore) GetTeamsByUserId(userId string) ([]*model.Team, error) {
teams := []*model.Team{}
query := s.teamsQuery.
Join("TeamMembers ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0})
if err := s.GetReplica().SelectBuilder(&teams, query); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetAllPrivateTeamListing returns all private teams.
func (s SqlTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
query := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": false}).
OrderBy("DisplayName")
data := []*model.Team{}
if err := s.GetReplica().SelectBuilder(&data, query); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetAllTeamListing returns all public teams.
func (s SqlTeamStore) GetAllTeamListing() ([]*model.Team, error) {
query := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": true}).
OrderBy("DisplayName")
data := []*model.Team{}
if err := s.GetReplica().SelectBuilder(&data, query); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// PermanentDelete permanently deletes from the database the team entry that matches the teamId passed as parameter.
// To soft-delete the team you can Update it with the DeleteAt field set to the current millisecond using model.GetMillis()
func (s SqlTeamStore) PermanentDelete(teamId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Teams").
Where(sq.Eq{"Id": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMaster().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Team with id=%s", teamId)
}
return nil
}
// AnalyticsTeamCount returns the total number of teams.
func (s SqlTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
query := s.getQueryBuilder().Select("COUNT(*) FROM Teams")
if opts == nil || (opts.IncludeDeleted != nil && !*opts.IncludeDeleted) {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if opts != nil && opts.AllowOpenInvite != nil {
query = query.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var c int64
err = s.GetReplica().Get(&c, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Teams")
}
return c, nil
}
func (s SqlTeamStore) getTeamMembersWithSchemeSelectQuery() sq.SelectBuilder {
query := s.teamMembersQuery.
Column("TeamScheme.DefaultTeamGuestRole TeamSchemeDefaultGuestRole").
Column("TeamScheme.DefaultTeamUserRole TeamSchemeDefaultUserRole").
Column("TeamScheme.DefaultTeamAdminRole TeamSchemeDefaultAdminRole").
LeftJoin("Teams ON TeamMembers.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id")
return query
}
func (s SqlTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
newTeamMembers := map[string]int{}
users := map[string]bool{}
for _, member := range members {
newTeamMembers[member.TeamId] = 0
}
for _, member := range members {
newTeamMembers[member.TeamId]++
users[member.UserId] = true
if err := member.IsValid(); err != nil {
return nil, err
}
}
teams := []string{}
for team := range newTeamMembers {
teams = append(teams, team)
}
defaultTeamRolesByTeam := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
queryRoles := s.getQueryBuilder().
Select(
"Teams.Id as Id",
"TeamScheme.DefaultTeamGuestRole as Guest",
"TeamScheme.DefaultTeamUserRole as User",
"TeamScheme.DefaultTeamAdminRole as Admin",
).
From("Teams").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Teams.Id": teams})
sqlRolesQuery, argsRoles, err := queryRoles.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_roles_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMaster().Select(&defaultTeamsRoles, sqlRolesQuery, argsRoles...)
if err != nil {
return nil, errors.Wrap(err, "default_team_roles_select")
}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByTeam[defaultRoles.Id] = defaultRoles
}
if maxUsersPerTeam >= 0 {
queryCount := s.getQueryBuilder().
Select(
"COUNT(0) as Count, TeamMembers.TeamId as TeamId",
).
From("TeamMembers").
Join("Users ON TeamMembers.UserId = Users.Id").
Where(sq.Eq{"TeamMembers.TeamId": teams}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0}).
Where(sq.Eq{"Users.DeleteAt": 0}).
GroupBy("TeamMembers.TeamId")
sqlCountQuery, argsCount, errCount := queryCount.ToSql()
if errCount != nil {
return nil, errors.Wrap(err, "member_count_tosql")
}
counters := []struct {
Count int
TeamId string
}{}
err = s.GetMaster().Select(&counters, sqlCountQuery, argsCount...)
if err != nil {
return nil, errors.Wrap(err, "failed to count users in the teams of the memberships")
}
for teamId, newMembers := range newTeamMembers {
existingMembers := 0
for _, counter := range counters {
if counter.TeamId == teamId {
existingMembers = counter.Count
}
}
if existingMembers+newMembers > maxUsersPerTeam {
return nil, store.NewErrLimitExceeded("TeamMember", existingMembers+newMembers, "team members limit exceeded")
}
}
}
query := s.getQueryBuilder().Insert("TeamMembers").Columns(teamMemberSliceColumns()...)
for _, member := range members {
query = query.Values(teamMemberToSlice(member)...)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "insert_members_to_sql")
}
if _, err = s.GetMaster().Exec(sql, args...); err != nil {
if IsUniqueConstraintError(err, []string{"TeamId", "teammembers_pkey", "PRIMARY"}) {
return nil, store.NewErrConflict("TeamMember", err, "")
}
return nil, errors.Wrap(err, "unable_to_save_team_member")
}
newMembers := []*model.TeamMember{}
for _, member := range members {
s.InvalidateAllTeamIdsForUser(member.UserId)
defaultTeamGuestRole := defaultTeamRolesByTeam[member.TeamId].Guest.String
defaultTeamUserRole := defaultTeamRolesByTeam[member.TeamId].User.String
defaultTeamAdminRole := defaultTeamRolesByTeam[member.TeamId].Admin.String
rolesResult := getTeamRoles(member.SchemeGuest, member.SchemeUser, member.SchemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(member.ExplicitRoles))
newMember := *member
newMember.SchemeGuest = rolesResult.schemeGuest
newMember.SchemeUser = rolesResult.schemeUser
newMember.SchemeAdmin = rolesResult.schemeAdmin
newMember.Roles = strings.Join(rolesResult.roles, " ")
newMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
newMembers = append(newMembers, &newMember)
}
return newMembers, nil
}
func (s SqlTeamStore) SaveMember(rctx request.CTX, member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
members, err := s.SaveMultipleMembers([]*model.TeamMember{member}, maxUsersPerTeam)
if err != nil {
return nil, err
}
return members[0], nil
}
func (s SqlTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
teams := []string{}
for _, member := range members {
member.PreUpdate()
newTeamMember := NewTeamMemberFromModel(member)
if err := member.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMaster().NamedExec(`UPDATE TeamMembers
SET Roles=:Roles, DeleteAt=:DeleteAt, CreateAt=:CreateAt, SchemeGuest=:SchemeGuest,
SchemeUser=:SchemeUser, SchemeAdmin=:SchemeAdmin
WHERE TeamId=:TeamId AND UserId=:UserId`, newTeamMember); err != nil {
return nil, errors.Wrap(err, "failed to update TeamMember")
}
teams = append(teams, member.TeamId)
}
query := s.getQueryBuilder().
Select(
"Teams.Id as Id",
"TeamScheme.DefaultTeamGuestRole as Guest",
"TeamScheme.DefaultTeamUserRole as User",
"TeamScheme.DefaultTeamAdminRole as Admin",
).
From("Teams").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Teams.Id": teams})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMaster().Select(&defaultTeamsRoles, sqlQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
defaultTeamRolesByTeam := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByTeam[defaultRoles.Id] = defaultRoles
}
updatedMembers := []*model.TeamMember{}
for _, member := range members {
s.InvalidateAllTeamIdsForUser(member.UserId)
defaultTeamGuestRole := defaultTeamRolesByTeam[member.TeamId].Guest.String
defaultTeamUserRole := defaultTeamRolesByTeam[member.TeamId].User.String
defaultTeamAdminRole := defaultTeamRolesByTeam[member.TeamId].Admin.String
rolesResult := getTeamRoles(member.SchemeGuest, member.SchemeUser, member.SchemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(member.ExplicitRoles))
updatedMember := *member
updatedMember.SchemeGuest = rolesResult.schemeGuest
updatedMember.SchemeUser = rolesResult.schemeUser
updatedMember.SchemeAdmin = rolesResult.schemeAdmin
updatedMember.Roles = strings.Join(rolesResult.roles, " ")
updatedMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
updatedMembers = append(updatedMembers, &updatedMember)
}
return updatedMembers, nil
}
func (s SqlTeamStore) UpdateMember(rctx request.CTX, member *model.TeamMember) (*model.TeamMember, error) {
members, err := s.UpdateMultipleMembers([]*model.TeamMember{member})
if err != nil {
return nil, err
}
return members[0], nil
}
// GetMember returns a single member of the team that matches the teamId and userId provided as parameters.
func (s SqlTeamStore) GetMember(rctx request.CTX, teamId string, userId string) (*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.UserId": userId})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
var dbMember teamMemberWithSchemeRoles
err = s.DBXFromContext(rctx.Context()).Get(&dbMember, queryString, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TeamMember", fmt.Sprintf("teamId=%s, userId=%s", teamId, userId))
}
return nil, errors.Wrapf(err, "failed to find TeamMembers with teamId=%s and userId=%s", teamId, userId)
}
return dbMember.ToModel(), nil
}
// GetMembers returns a list of members from the database that matches the teamId passed as parameter and,
// also expects teamMembersGetOptions to be passed as a parameter which allows to further filter what to show in the result.
// TeamMembersGetOptions Model has following options->
// 1. Sort through USERNAME [ if provided, which otherwise defaults to ID ]
// 2. Sort through USERNAME [ if provided, which otherwise defaults to ID ] and exclude deleted members.
// 3. Return all the members but, exclude deleted ones.
// 4. Apply ViewUsersRestrictions to restrict what is visible to the user.
func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0}).
Limit(uint64(limit)).
Offset(uint64(offset))
if teamMembersGetOptions == nil || teamMembersGetOptions.Sort == "" {
query = query.OrderBy("UserId")
}
if teamMembersGetOptions != nil {
if teamMembersGetOptions.Sort == model.USERNAME || teamMembersGetOptions.ExcludeDeletedUsers {
query = query.LeftJoin("Users ON TeamMembers.UserId = Users.Id")
}
if teamMembersGetOptions.ExcludeDeletedUsers {
query = query.Where(sq.Eq{"Users.DeleteAt": 0})
}
if teamMembersGetOptions.Sort == model.USERNAME {
query = query.OrderBy(model.USERNAME)
}
query = applyTeamMemberViewRestrictionsFilter(query, teamMembersGetOptions.ViewRestrictions)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.GetReplica().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with teamId=%s", teamId)
}
return dbMembers.ToModel(), nil
}
// GetTotalMemberCount returns the number of all members in a team for the teamId passed as a parameter.
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetTotalMemberCount(teamId string, restrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("count(DISTINCT TeamMembers.UserId)").
From("TeamMembers, Users").
Where("TeamMembers.DeleteAt = 0").
Where("TeamMembers.UserId = Users.Id").
Where(sq.Eq{"TeamMembers.TeamId": teamId})
query = applyTeamMemberViewRestrictionsFilterForStats(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplica().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count TeamMembers")
}
return count, nil
}
// GetActiveMemberCount returns the number of active members in a team for the teamId passed as a parameter i.e. members with 'DeleteAt = 0'
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetActiveMemberCount(teamId string, restrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("count(DISTINCT TeamMembers.UserId)").
From("TeamMembers, Users").
Where("TeamMembers.DeleteAt = 0").
Where("TeamMembers.UserId = Users.Id").
Where("Users.DeleteAt = 0").
Where(sq.Eq{"TeamMembers.TeamId": teamId})
query = applyTeamMemberViewRestrictionsFilterForStats(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplica().Get(&count, queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count TeamMembers")
}
return count, nil
}
// GetMembersByIds returns a list of members from the database that matches the teamId and the list of userIds passed as parameters.
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
if len(userIds) == 0 {
return nil, errors.New("invalid list of user ids")
}
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.UserId": userIds}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0})
query = applyTeamMemberViewRestrictionsFilter(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
if err = s.GetReplica().Select(&dbMembers, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
return dbMembers.ToModel(), nil
}
// GetTeamsForUser returns a list of teams that the user is a member of. Expects userId to be passed as a parameter. It can also negative the teamID passed.
func (s SqlTeamStore) GetTeamsForUser(rctx request.CTX, userId, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.UserId": userId})
if excludeTeamID != "" {
query = query.Where(sq.NotEq{"TeamMembers.TeamId": excludeTeamID})
}
if !includeDeleted {
query = query.Where(sq.Eq{"TeamMembers.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.SqlStore.DBXFromContext(rctx.Context()).Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
// GetTeamsForUserWithPagination returns limited TeamMembers according to the perPage parameter specified.
// It also offsets the records as per the page parameter supplied.
func (s SqlTeamStore) GetTeamsForUserWithPagination(userId string, page, perPage int) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.UserId": userId}).
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.GetReplica().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
// GetChannelUnreadsForAllTeams returns unreads msg count, mention counts, and notifyProps
// for all the channels in all the teams except the excluded ones.
func (s SqlTeamStore) GetChannelUnreadsForAllTeams(excludeTeamId, userId string) ([]*model.ChannelUnread, error) {
query, args, err := s.getQueryBuilder().
Select("Channels.TeamId TeamId", "Channels.Id ChannelId", "(Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount", "(Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot", "ChannelMembers.MentionCount MentionCount", "ChannelMembers.MentionCountRoot MentionCountRoot", "ChannelMembers.NotifyProps NotifyProps").
From("Channels").
Join("ChannelMembers ON Id = ChannelId").
Where(sq.Eq{"UserId": userId, "DeleteAt": 0}).
Where(sq.NotEq{"TeamId": excludeTeamId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
data := []*model.ChannelUnread{}
err = s.GetReplica().Select(&data, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with userId=%s and teamId!=%s", userId, excludeTeamId)
}
return data, nil
}
// GetChannelUnreadsForTeam returns unreads msg count, mention counts and notifyProps for all the channels in a single team.
func (s SqlTeamStore) GetChannelUnreadsForTeam(teamId, userId string) ([]*model.ChannelUnread, error) {
query, args, err := s.getQueryBuilder().
Select("Channels.TeamId TeamId", "Channels.Id ChannelId", "(Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount", "(Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot", "ChannelMembers.MentionCount MentionCount", "ChannelMembers.MentionCountRoot MentionCountRoot", "ChannelMembers.NotifyProps NotifyProps").
From("Channels").
Join("ChannelMembers ON Id = ChannelId").
Where(sq.Eq{"UserId": userId, "TeamId": teamId, "DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
channels := []*model.ChannelUnread{}
err = s.GetReplica().Select(&channels, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with teamId=%s and userId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlTeamStore) RemoveMembers(rctx request.CTX, teamId string, userIds []string) error {
builder := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"TeamId": teamId}).
Where(sq.Eq{"UserId": userIds})
query, args, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMaster().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with teamId=%s and userId in %v", teamId, userIds)
}
return nil
}
// RemoveMember remove from the database the team members that match the userId and teamId passed as parameter.
func (s SqlTeamStore) RemoveMember(rctx request.CTX, teamId string, userId string) error {
return s.RemoveMembers(rctx, teamId, []string{userId})
}
// RemoveAllMembersByTeam removes from the database the team members that belong to the teamId passed as parameter.
func (s SqlTeamStore) RemoveAllMembersByTeam(teamId string) error {
query, args, err := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"TeamId": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMaster().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with teamId=%s", teamId)
}
return nil
}
// RemoveAllMembersByUser removes from the database the team members that match the userId passed as parameter.
func (s SqlTeamStore) RemoveAllMembersByUser(rctx request.CTX, userId string) error {
query, args, err := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"UserId": userId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMaster().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with userId=%s", userId)
}
return nil
}
// UpdateLastTeamIconUpdate sets the last updated time for the icon based on the parameter passed in teamId. The
// LastTeamIconUpdate and UpdateAt fields are set to the parameter passed in curTime. Returns nil on success and an error
// otherwise.
func (s SqlTeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) error {
query, args, err := s.getQueryBuilder().
Update("Teams").
SetMap(sq.Eq{"LastTeamIconUpdate": curTime, "UpdateAt": curTime}).
Where(sq.Eq{"Id": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMaster().Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to update Team")
}
return nil
}
// GetTeamsByScheme returns from the database all teams that match the schemeId provided as parameter, up to
// a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"SchemeId": schemeId}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
teams := []*model.Team{}
err = s.GetReplica().Select(&teams, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Teams with schemeId=%s", schemeId)
}
return teams, nil
}
// MigrateTeamMembers performs the Advanced Permissions Phase 2 migration for TeamMember objects. Migration is done
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlTeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) (_ map[string]string, err error) {
var transaction *sqlxTxWrapper
if transaction, err = s.GetMaster().Beginx(); err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query := s.teamMembersQuery.
Where(sq.Expr("(TeamMembers.TeamId, TeamMembers.UserId) > (?, ?)", fromTeamId, fromUserId)).
OrderBy("TeamMembers.TeamId, TeamMembers.UserId").
Limit(100)
teamMembers := []teamMember{}
if err := transaction.SelectBuilder(&teamMembers, query); err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
if len(teamMembers) == 0 {
// No more team members in query result means that the migration has finished.
return nil, nil
}
for i := range teamMembers {
member := teamMembers[i]
roles := strings.Fields(member.Roles)
var newRoles []string
if !member.SchemeAdmin.Valid {
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeUser.Valid {
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeGuest.Valid {
member.SchemeGuest = sql.NullBool{Bool: false, Valid: true}
}
for _, role := range roles {
if role == model.TeamAdminRoleId {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.TeamUserRoleId {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else if role == model.TeamGuestRoleId {
member.SchemeGuest = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.NamedExec(`UPDATE TeamMembers
SET TeamId=:TeamId,
UserId=:UserId,
Roles=:Roles,
DeleteAt=:DeleteAt,
SchemeUser=:SchemeUser,
SchemeAdmin=:SchemeAdmin,
SchemeGuest=:SchemeGuest
WHERE TeamId=:TeamId AND UserId=:UserId`, &member); err != nil {
return nil, errors.Wrap(err, "failed to update TeamMember")
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
data := make(map[string]string)
data["TeamId"] = teamMembers[len(teamMembers)-1].TeamId
data["UserId"] = teamMembers[len(teamMembers)-1].UserId
return data, nil
}
// ResetAllTeamSchemes Set all Team's SchemeId values to an empty string.
func (s SqlTeamStore) ResetAllTeamSchemes() error {
if _, err := s.GetMaster().Exec("UPDATE Teams SET SchemeId=''"); err != nil {
return errors.Wrap(err, "failed to update Teams")
}
return nil
}
// ClearCaches method not implemented.
func (s SqlTeamStore) ClearCaches() {}
// InvalidateAllTeamIdsForUser does not execute anything because the store does not handle the cache.
//
//nolint:unparam
func (s SqlTeamStore) InvalidateAllTeamIdsForUser(userId string) {}
// ClearAllCustomRoleAssignments removes all custom role assignments from TeamMembers.
func (s SqlTeamStore) ClearAllCustomRoleAssignments() (err error) {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
lastTeamId := strings.Repeat("0", 26)
for {
var transaction *sqlxTxWrapper
var err error
if transaction, err = s.GetMaster().Beginx(); err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query := s.teamMembersQuery.
Where(sq.Expr("(TeamMembers.TeamId, TeamMembers.UserId) > (?, ?)", lastTeamId, lastUserId)).
OrderBy("TeamMembers.TeamId, TeamMembers.UserId").
Limit(1000)
teamMembers := []*teamMember{}
if err := transaction.SelectBuilder(&teamMembers, query); err != nil {
return errors.Wrap(err, "failed to find TeamMembers")
}
if len(teamMembers) == 0 {
break
}
for _, member := range teamMembers {
lastUserId = member.UserId
lastTeamId = member.TeamId
var newRoles []string
for role := range strings.FieldsSeq(member.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != member.Roles {
if _, err := transaction.Exec("UPDATE TeamMembers SET Roles = ? WHERE UserId = ? AND TeamId = ?", newRolesString, member.UserId, member.TeamId); err != nil {
return errors.Wrap(err, "failed to update TeamMembers")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
// AnalyticsGetTeamCountForScheme returns the number of active teams that match the schemeId passed as parameter.
func (s SqlTeamStore) AnalyticsGetTeamCountForScheme(schemeId string) (int64, error) {
query, args, err := s.getQueryBuilder().
Select("count(*)").
From("Teams").
Where(sq.Eq{"SchemeId": schemeId, "DeleteAt": 0}).ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplica().Get(&count, query, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count Teams with schemeId=%s", schemeId)
}
return count, nil
}
// GetAllForExportAfter returns teams for export, up to a total limit passed as parameter where Teams.Id is greater than the afterId passed as parameter.
func (s SqlTeamStore) GetAllForExportAfter(limit int, afterId string) ([]*model.TeamForExport, error) {
data := []*model.TeamForExport{}
query, args, err := s.getQueryBuilder().
Select(teamSliceColumns()...).
Column("Schemes.Name as SchemeName").
From("Teams").
LeftJoin("Schemes ON Teams.SchemeId = Schemes.Id").
Where(sq.Gt{"Teams.Id": afterId}).
OrderBy("Id").
Limit(uint64(limit)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplica().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetUserTeamIds get the team ids to which the user belongs to. allowFromCache parameter does not have any effect in this Store
//
//nolint:unparam
func (s SqlTeamStore) GetUserTeamIds(userId string, allowFromCache bool) ([]string, error) {
teamIds := []string{}
query, args, err := s.getQueryBuilder().
Select("TeamId").
From("TeamMembers").
Join("Teams ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return []string{}, errors.Wrap(err, "team_tosql")
}
err = s.GetReplica().Select(&teamIds, query, args...)
if err != nil {
return []string{}, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return teamIds, nil
}
// GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified
// users belong.
func (s SqlTeamStore) GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error) {
var teamIDs []string
query, args, err := s.getQueryBuilder().
Select("TM1.TeamId").
From("TeamMembers AS TM1").
InnerJoin("TeamMembers AS TM2 ON TM1.TeamId = TM2.TeamId").
InnerJoin("Teams ON TM1.TeamId = Teams.Id").
Where(sq.And{
sq.Eq{"TM1.UserId": userID},
sq.Eq{"TM1.DeleteAt": 0},
sq.Eq{"TM2.UserId": otherUserID},
sq.Eq{"TM2.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplica().Select(&teamIDs, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with user IDs %s and %s", userID, otherUserID)
}
return teamIDs, nil
}
func (s SqlTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) {
subQuery := s.getSubQueryBuilder().
Select("TeamId, UserId").
From("TeamMembers").
Where(sq.Eq{
"UserId": userIDs,
"DeleteAt": 0,
})
subQuerySQL, subQueryParams, err := subQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetCommonTeamIDsForMultipleUsers_subquery_toSQL")
}
query := s.getQueryBuilder().
Select("t.Id").
From("Teams AS t").
Join("("+subQuerySQL+") AS tm ON t.Id = tm.TeamId", subQueryParams...).
Where(sq.Eq{
"t.DeleteAt": 0,
}).
GroupBy("t.Id").
Having(sq.Eq{
"COUNT(UserId)": len(userIDs),
})
querySQL, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetCommonTeamIDsForMultipleUsers_query_toSQL")
}
var teamIDs []string
if err := s.GetReplica().Select(&teamIDs, querySQL, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find common team for users %v", userIDs)
}
return teamIDs, nil
}
// GetTeamMembersForExport gets the various teams for which a user, denoted by userId, is a part of.
func (s SqlTeamStore) GetTeamMembersForExport(userId string) ([]*model.TeamMemberForExport, error) {
members := []*model.TeamMemberForExport{}
query, args, err := s.getQueryBuilder().
Select("TeamMembers.TeamId", "TeamMembers.UserId", "TeamMembers.Roles", "TeamMembers.DeleteAt",
"(TeamMembers.SchemeGuest IS NOT NULL AND TeamMembers.SchemeGuest) as SchemeGuest",
"TeamMembers.SchemeUser", "TeamMembers.SchemeAdmin", "Teams.Name as TeamName").
From("TeamMembers").
Join("Teams ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplica().Select(&members, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return members, nil
}
// UserBelongsToTeams returns true if the user denoted by userId is a member of the teams in the teamIds string array.
func (s SqlTeamStore) UserBelongsToTeams(userId string, teamIds []string) (bool, error) {
idQuery := sq.Eq{
"UserId": userId,
"TeamId": teamIds,
"DeleteAt": 0,
}
query, params, err := s.getQueryBuilder().Select("Count(*)").From("TeamMembers").Where(idQuery).ToSql()
if err != nil {
return false, errors.Wrap(err, "team_tosql")
}
var c int64
err = s.GetReplica().Get(&c, query, params...)
if err != nil {
return false, errors.Wrap(err, "failed to count TeamMembers")
}
return c > 0, nil
}
// UpdateMembersRole updates all the members of teamID in the adminIDs string array to be admins and sets all other
// users as not being admin.
// It returns the list of userIDs whose roles got updated.
func (s SqlTeamStore) UpdateMembersRole(teamID string, adminIDs []string) (_ []*model.TeamMember, err error) {
transaction, err := s.GetMaster().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(transaction, &err)
// TODO: https://mattermost.atlassian.net/browse/MM-63368
// On MySQL it's not possible to update a table and select from it in the same query.
// A SELECT and a UPDATE query are needed.
// Once we only support PostgreSQL, this can be done in a single query using RETURNING.
query, args, err := s.teamMembersQuery.
Where(sq.Eq{"TeamId": teamID, "DeleteAt": 0}).
Where(sq.Or{sq.Eq{"SchemeGuest": false}, sq.Expr("SchemeGuest IS NULL")}).
Where(
sq.Or{
// New admins
sq.And{
sq.Eq{"SchemeAdmin": false},
sq.Eq{"UserId": adminIDs},
},
// Demoted admins
sq.And{
sq.Eq{"SchemeAdmin": true},
sq.NotEq{"UserId": adminIDs},
},
},
).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
var updatedMembers []*model.TeamMember
if err = transaction.Select(&updatedMembers, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get list of updated users")
}
// Update SchemeAdmin field as the data from the SQL is not updated yet
for _, member := range updatedMembers {
if slices.Contains(adminIDs, member.UserId) {
member.SchemeAdmin = true
} else {
member.SchemeAdmin = false
}
}
query, args, err = s.getQueryBuilder().
Update("TeamMembers").
Set("SchemeAdmin", sq.Case().When(sq.Eq{"UserId": adminIDs}, "true").Else("false")).
Where(sq.Eq{"TeamId": teamID, "DeleteAt": 0}).
Where(sq.Or{sq.Eq{"SchemeGuest": false}, sq.Expr("SchemeGuest IS NULL")}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if _, err = transaction.Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to update TeamMembers")
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return updatedMembers, nil
}
func applyTeamMemberViewRestrictionsFilter(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query.Join("Users ru ON (TeamMembers.UserId = ru.Id)")
if len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = ru.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = ru.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
return resultQuery.Distinct()
}
func applyTeamMemberViewRestrictionsFilterForStats(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query
if len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = Users.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = Users.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
return resultQuery
}
// GroupSyncedTeamCount returns the number of teams that are group constrained.
func (s SqlTeamStore) GroupSyncedTeamCount() (int64, error) {
builder := s.getQueryBuilder().Select("COUNT(*)").From("Teams").Where(sq.Eq{"GroupConstrained": true, "DeleteAt": 0})
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplica().Get(&count, query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Teams")
}
return count, nil
}