// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package model import ( "net/http" "regexp" "strings" ) const ( GroupSourceLdap GroupSource = "ldap" GroupSourceCustom GroupSource = "custom" // plugin groups must prefix their source with this GroupSourcePluginPrefix GroupSource = "plugin_" GroupNameMaxLength = 64 GroupSourceMaxLength = 64 GroupDisplayNameMaxLength = 128 GroupDescriptionMaxLength = 1024 GroupRemoteIDMaxLength = 48 ) type GroupSource string type Group struct { Id string `json:"id"` Name *string `json:"name,omitempty"` DisplayName string `json:"display_name"` Description string `json:"description"` Source GroupSource `json:"source"` RemoteId *string `json:"remote_id"` CreateAt int64 `json:"create_at"` UpdateAt int64 `json:"update_at"` DeleteAt int64 `json:"delete_at"` HasSyncables bool `db:"-" json:"has_syncables"` MemberCount *int `db:"-" json:"member_count,omitempty"` AllowReference bool `json:"allow_reference"` ChannelMemberCount *int `db:"-" json:"channel_member_count,omitempty"` ChannelMemberTimezonesCount *int `db:"-" json:"channel_member_timezones_count,omitempty"` MemberIDs []string `db:"-" json:"member_ids"` } func (group *Group) Auditable() map[string]any { return map[string]any{ "id": group.Id, "source": group.Source, "remote_id": group.GetRemoteId(), "create_at": group.CreateAt, "update_at": group.UpdateAt, "delete_at": group.DeleteAt, "has_syncables": group.HasSyncables, "member_count": group.GetMemberCount(), "allow_reference": group.AllowReference, } } func (group *Group) LogClone() any { return map[string]any{ "id": group.Id, "name": group.GetName(), "display_name": group.DisplayName, "source": group.Source, "remote_id": group.GetRemoteId(), "create_at": group.CreateAt, "update_at": group.UpdateAt, "delete_at": group.DeleteAt, "has_syncables": group.HasSyncables, "member_count": group.GetMemberCount(), "allow_reference": group.AllowReference, } } type GroupWithUserIds struct { Group UserIds []string `json:"user_ids"` } func (group *GroupWithUserIds) Auditable() map[string]any { return map[string]any{ "id": group.Id, "source": group.Source, "remote_id": group.GetRemoteId(), "create_at": group.CreateAt, "update_at": group.UpdateAt, "delete_at": group.DeleteAt, "has_syncables": group.HasSyncables, "member_count": group.GetMemberCount(), "allow_reference": group.AllowReference, "user_ids": group.UserIds, } } type GroupWithSchemeAdmin struct { Group SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"` } type GroupsAssociatedToChannelWithSchemeAdmin struct { ChannelId string `json:"channel_id"` Group SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"` } type GroupsAssociatedToChannel struct { ChannelId string `json:"channel_id"` Groups []*GroupWithSchemeAdmin `json:"groups"` } type GroupPatch struct { Name *string `json:"name"` DisplayName *string `json:"display_name"` Description *string `json:"description"` AllowReference *bool `json:"allow_reference"` // For security reasons (including preventing unintended LDAP group synchronization) do no allow a Group's RemoteId or Source field to be // included in patches. } type LdapGroupSearchOpts struct { Q string IsLinked *bool IsConfigured *bool } type GroupSearchOpts struct { Q string NotAssociatedToTeam string NotAssociatedToChannel string IncludeMemberCount bool FilterAllowReference bool PageOpts *PageOpts Since int64 Source GroupSource // FilterParentTeamPermitted filters the groups to the intersect of the // set associated to the parent team and those returned by the query. // If the parent team is not group-constrained or if NotAssociatedToChannel // is not set then this option is ignored. FilterParentTeamPermitted bool // FilterHasMember filters the groups to the intersect of the // set returned by the query and those that have the given user as a member. FilterHasMember string IncludeChannelMemberCount string IncludeTimezones bool IncludeMemberIDs bool // Include archived groups IncludeArchived bool // Only return archived groups FilterArchived bool // OnlySyncableSources filters the groups to only those that are syncable OnlySyncableSources bool } type GetGroupOpts struct { IncludeMemberCount bool IncludeMemberIDs bool } type PageOpts struct { Page int PerPage int } type GroupStats struct { GroupID string `json:"group_id"` TotalMemberCount int64 `json:"total_member_count"` } type GroupModifyMembers struct { UserIds []string `json:"user_ids"` } func (group *GroupModifyMembers) Auditable() map[string]any { return map[string]any{ "user_ids": group.UserIds, } } func (group *Group) Patch(patch *GroupPatch) { if patch.Name != nil { group.Name = patch.Name } if patch.DisplayName != nil { group.DisplayName = *patch.DisplayName } if patch.Description != nil { group.Description = *patch.Description } if patch.AllowReference != nil { group.AllowReference = *patch.AllowReference } } func (group *Group) IsValidForCreate() *AppError { appErr := group.IsValidName() if appErr != nil { return appErr } if l := len(group.DisplayName); l == 0 || l > GroupDisplayNameMaxLength { return NewAppError("Group.IsValidForCreate", "model.group.display_name.app_error", map[string]any{"GroupDisplayNameMaxLength": GroupDisplayNameMaxLength}, "", http.StatusBadRequest) } if len(group.Description) > GroupDescriptionMaxLength { return NewAppError("Group.IsValidForCreate", "model.group.description.app_error", map[string]any{"GroupDescriptionMaxLength": GroupDescriptionMaxLength}, "", http.StatusBadRequest) } isValidSource := false if group.Source == GroupSourceLdap || group.Source == GroupSourceCustom || strings.HasPrefix(string(group.Source), string(GroupSourcePluginPrefix)) { isValidSource = true } if !isValidSource { return NewAppError("Group.IsValidForCreate", "model.group.source.app_error", nil, "", http.StatusBadRequest) } if (group.GetRemoteId() == "" && group.requiresRemoteId()) || len(group.GetRemoteId()) > GroupRemoteIDMaxLength { return NewAppError("Group.IsValidForCreate", "model.group.remote_id.app_error", nil, "", http.StatusBadRequest) } return nil } func (group *Group) requiresRemoteId() bool { return group.Source == GroupSourceLdap || strings.HasPrefix(string(group.Source), string(GroupSourcePluginPrefix)) } func GetSyncableGroupSources() []GroupSource { return []GroupSource{GroupSourceLdap} } func GetSyncableGroupSourcePrefixes() []GroupSource { return []GroupSource{GroupSourcePluginPrefix} } func (group *Group) IsSyncable() bool { return group.Source == GroupSourceLdap || strings.HasPrefix(string(group.Source), string(GroupSourcePluginPrefix)) } func (group *Group) IsValidForUpdate() *AppError { if !IsValidId(group.Id) { return NewAppError("Group.IsValidForUpdate", "app.group.id.app_error", nil, "", http.StatusBadRequest) } if group.CreateAt == 0 { return NewAppError("Group.IsValidForUpdate", "model.group.create_at.app_error", nil, "", http.StatusBadRequest) } if group.UpdateAt == 0 { return NewAppError("Group.IsValidForUpdate", "model.group.update_at.app_error", nil, "", http.StatusBadRequest) } if appErr := group.IsValidForCreate(); appErr != nil { return appErr } return nil } var validGroupnameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`) func (group *Group) IsValidName() *AppError { if group.Name == nil { if group.AllowReference { return NewAppError("Group.IsValidName", "model.group.name.app_error", map[string]any{"GroupNameMaxLength": GroupNameMaxLength}, "", http.StatusBadRequest) } } else { if l := len(*group.Name); l == 0 || l > GroupNameMaxLength { return NewAppError("Group.IsValidName", "model.group.name.invalid_length.app_error", map[string]any{"GroupNameMaxLength": GroupNameMaxLength}, "", http.StatusBadRequest) } if *group.Name == UserNotifyAll || *group.Name == ChannelMentionsNotifyProp || *group.Name == UserNotifyHere { return NewAppError("IsValidName", "model.group.name.reserved_name.app_error", nil, "", http.StatusBadRequest) } if !validGroupnameChars.MatchString(*group.Name) { return NewAppError("Group.IsValidName", "model.group.name.invalid_chars.app_error", nil, "", http.StatusBadRequest) } } return nil } func (group *Group) GetName() string { return SafeDereference(group.Name) } func (group *Group) GetRemoteId() string { return SafeDereference(group.RemoteId) } func (group *Group) GetMemberCount() int { return SafeDereference(group.MemberCount) } type GroupsWithCount struct { Groups []*Group `json:"groups"` TotalCount int64 `json:"total_count"` } type CreateDefaultMembershipParams struct { Since int64 ReAddRemovedMembers bool ScopedUserID *string ScopedTeamID *string ScopedChannelID *string }