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>
887 lines
32 KiB
Go
887 lines
32 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
)
|
|
|
|
const (
|
|
EmojisPermissionsMigrationKey = "EmojisPermissionsMigrationComplete"
|
|
GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete"
|
|
SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete"
|
|
CustomGroupAdminRoleCreationMigrationKey = "CustomGroupAdminRoleCreationMigrationComplete"
|
|
ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete"
|
|
PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete"
|
|
FirstAdminSetupCompleteKey = model.SystemFirstAdminSetupComplete
|
|
remainingSchemaMigrationsKey = "RemainingSchemaMigrations"
|
|
postPriorityConfigDefaultTrueMigrationKey = "PostPriorityConfigDefaultTrueMigrationComplete"
|
|
contentFlaggingSetupDoneKey = "content_flagging_setup_done"
|
|
contentFlaggingMigrationVersion = "v5"
|
|
|
|
contentFlaggingPropertyNameFlaggedPostId = "flagged_post_id"
|
|
ContentFlaggingPropertyNameStatus = "status"
|
|
contentFlaggingPropertyNameReportingUserID = "reporting_user_id"
|
|
contentFlaggingPropertyNameReportingReason = "reporting_reason"
|
|
contentFlaggingPropertyNameReportingComment = "reporting_comment"
|
|
contentFlaggingPropertyNameReportingTime = "reporting_time"
|
|
contentFlaggingPropertyNameReviewerUserID = "reviewer_user_id"
|
|
contentFlaggingPropertyNameActorUserID = "actor_user_id"
|
|
contentFlaggingPropertyNameActorComment = "actor_comment"
|
|
contentFlaggingPropertyNameActionTime = "action_time"
|
|
contentFlaggingPropertyManageByContentFlagging = "content_flagging_managed"
|
|
|
|
contentFlaggingPropertySubTypeTimestamp = "timestamp"
|
|
)
|
|
|
|
// This function migrates the default built in roles from code/config to the database.
|
|
func (a *App) DoAdvancedPermissionsMigration() error {
|
|
return a.Srv().doAdvancedPermissionsMigration()
|
|
}
|
|
|
|
func (s *Server) doAdvancedPermissionsMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(model.AdvancedPermissionsMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
mlog.Info("Migrating roles to database.")
|
|
roles := model.MakeDefaultRoles()
|
|
|
|
var multiErr *multierror.Error
|
|
for _, role := range roles {
|
|
_, err := s.Store().Role().Save(role)
|
|
if err == nil {
|
|
continue
|
|
}
|
|
mlog.Warn("Couldn't save the role for advanced permissions migration, this can be an expected case", mlog.Err(err))
|
|
|
|
// If this failed for reasons other than the role already existing, don't mark the migration as done.
|
|
fetchedRole, err := s.Store().Role().GetByName(context.Background(), role.Name)
|
|
if err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to migrate role to database: %w", err))
|
|
continue
|
|
}
|
|
|
|
// If the role already existed, check it is the same and update if not.
|
|
if !reflect.DeepEqual(fetchedRole.Permissions, role.Permissions) ||
|
|
fetchedRole.DisplayName != role.DisplayName ||
|
|
fetchedRole.Description != role.Description ||
|
|
fetchedRole.SchemeManaged != role.SchemeManaged {
|
|
role.Id = fetchedRole.Id
|
|
if _, err = s.Store().Role().Save(role); err != nil {
|
|
// Role is not the same, but failed to update.
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to migrate role to database: %w", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
if multiErr != nil {
|
|
return multiErr
|
|
}
|
|
|
|
config := s.platform.Config()
|
|
*config.ServiceSettings.PostEditTimeLimit = -1
|
|
if _, _, err := s.platform.SaveConfig(config, true); err != nil {
|
|
return fmt.Errorf("failed to update config in Advanced Permissions Phase 1 Migration: %w", err)
|
|
}
|
|
|
|
system := model.System{
|
|
Name: model.AdvancedPermissionsMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark advanced permissions migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SetPhase2PermissionsMigrationStatus(isComplete bool) error {
|
|
if !isComplete {
|
|
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.MigrationKeyAdvancedPermissionsPhase2); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
a.Srv().phase2PermissionsMigrationComplete = isComplete
|
|
return nil
|
|
}
|
|
|
|
func (a *App) DoEmojisPermissionsMigration() error {
|
|
if err := a.Srv().doEmojisPermissionsMigration(); err != nil {
|
|
return fmt.Errorf("Failed to complete emojis permissions migration: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doEmojisPermissionsMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(EmojisPermissionsMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
var role *model.Role
|
|
var systemAdminRole *model.Role
|
|
var err *model.AppError
|
|
|
|
mlog.Info("Migrating emojis config to database.")
|
|
|
|
// Emoji creation is set to all by default
|
|
role, err = s.GetRoleByName(context.Background(), model.SystemUserRoleId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get role for system user: %w", err)
|
|
}
|
|
|
|
if role != nil {
|
|
role.Permissions = append(role.Permissions, model.PermissionCreateEmojis.Id, model.PermissionDeleteEmojis.Id)
|
|
if _, nErr := s.Store().Role().Save(role); nErr != nil {
|
|
return fmt.Errorf("failed to save role: %w", nErr)
|
|
}
|
|
}
|
|
|
|
systemAdminRole, err = s.GetRoleByName(context.Background(), model.SystemAdminRoleId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get role for system admin: %w", err)
|
|
}
|
|
|
|
systemAdminRole.Permissions = append(systemAdminRole.Permissions,
|
|
model.PermissionCreateEmojis.Id,
|
|
model.PermissionDeleteEmojis.Id,
|
|
model.PermissionDeleteOthersEmojis.Id,
|
|
)
|
|
if _, err := s.Store().Role().Save(systemAdminRole); err != nil {
|
|
return fmt.Errorf("failed to save role: %w", err)
|
|
}
|
|
|
|
system := model.System{
|
|
Name: EmojisPermissionsMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark emojis permissions migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) DoGuestRolesCreationMigration() error {
|
|
if err := a.Srv().doGuestRolesCreationMigration(); err != nil {
|
|
return fmt.Errorf("Failed to complete guest roles creation migration: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doGuestRolesCreationMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(GuestRolesCreationMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
roles := model.MakeDefaultRoles()
|
|
var multiErr *multierror.Error
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.ChannelGuestRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.ChannelGuestRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new guest role to database: %w", err))
|
|
}
|
|
}
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.TeamGuestRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.TeamGuestRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new guest role to database: %w", err))
|
|
}
|
|
}
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemGuestRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.SystemGuestRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new guest role to database: %w", err))
|
|
}
|
|
}
|
|
|
|
schemes, err := s.Store().Scheme().GetAllPage("", 0, 1000000)
|
|
if err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to get all schemes: %w", err))
|
|
}
|
|
for _, scheme := range schemes {
|
|
if scheme.DefaultTeamGuestRole == "" || scheme.DefaultChannelGuestRole == "" {
|
|
if scheme.Scope == model.SchemeScopeTeam {
|
|
// Team Guest Role
|
|
teamGuestRole := &model.Role{
|
|
Name: model.NewId(),
|
|
DisplayName: fmt.Sprintf("Team Guest Role for Scheme %s", scheme.Name),
|
|
Permissions: roles[model.TeamGuestRoleId].Permissions,
|
|
SchemeManaged: true,
|
|
}
|
|
|
|
if savedRole, err := s.Store().Role().Save(teamGuestRole); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new guest role for custom scheme: %w", err))
|
|
} else {
|
|
scheme.DefaultTeamGuestRole = savedRole.Name
|
|
}
|
|
}
|
|
|
|
// Channel Guest Role
|
|
channelGuestRole := &model.Role{
|
|
Name: model.NewId(),
|
|
DisplayName: fmt.Sprintf("Channel Guest Role for Scheme %s", scheme.Name),
|
|
Permissions: roles[model.ChannelGuestRoleId].Permissions,
|
|
SchemeManaged: true,
|
|
}
|
|
|
|
if savedRole, err := s.Store().Role().Save(channelGuestRole); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new guest role for custom scheme: %w", err))
|
|
} else {
|
|
scheme.DefaultChannelGuestRole = savedRole.Name
|
|
}
|
|
|
|
_, err := s.Store().Scheme().Save(scheme)
|
|
if err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to update custom scheme: %w", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
if multiErr != nil {
|
|
return multiErr
|
|
}
|
|
|
|
system := model.System{
|
|
Name: GuestRolesCreationMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark guest roles creation migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) DoSystemConsoleRolesCreationMigration() error {
|
|
return a.Srv().doSystemConsoleRolesCreationMigration()
|
|
}
|
|
|
|
func (s *Server) doSystemConsoleRolesCreationMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(SystemConsoleRolesCreationMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
roles := model.MakeDefaultRoles()
|
|
var multiErr *multierror.Error
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemManagerRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.SystemManagerRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new role %q: %w", model.SystemManagerRoleId, err))
|
|
}
|
|
}
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemReadOnlyAdminRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.SystemReadOnlyAdminRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new role %q: %w", model.SystemReadOnlyAdminRoleId, err))
|
|
}
|
|
}
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemUserManagerRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.SystemUserManagerRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new role %q: %w", model.SystemUserManagerRoleId, err))
|
|
}
|
|
}
|
|
|
|
if multiErr != nil {
|
|
return multiErr
|
|
}
|
|
|
|
system := model.System{
|
|
Name: SystemConsoleRolesCreationMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark system console roles creation migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doCustomGroupAdminRoleCreationMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(CustomGroupAdminRoleCreationMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
roles := model.MakeDefaultRoles()
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemCustomGroupAdminRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.SystemCustomGroupAdminRoleId]); err != nil {
|
|
return fmt.Errorf("failed to create new role %s: %w", model.SystemCustomGroupAdminRoleId, err)
|
|
}
|
|
}
|
|
|
|
system := model.System{
|
|
Name: CustomGroupAdminRoleCreationMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark custom group admin role creation migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doContentExtractionConfigDefaultTrueMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(ContentExtractionConfigDefaultTrueMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
s.platform.UpdateConfig(func(config *model.Config) {
|
|
config.FileSettings.ExtractContent = model.NewPointer(true)
|
|
})
|
|
|
|
system := model.System{
|
|
Name: ContentExtractionConfigDefaultTrueMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark content extraction config migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doPlaybooksRolesCreationMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(PlaybookRolesCreationMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
roles := model.MakeDefaultRoles()
|
|
var multiErr *multierror.Error
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookAdminRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.PlaybookAdminRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new playbook %q role to database: %w", model.PlaybookAdminRoleId, err))
|
|
}
|
|
}
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookMemberRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.PlaybookMemberRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new playbook %q role to database: %w", model.PlaybookMemberRoleId, err))
|
|
}
|
|
}
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.RunAdminRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.RunAdminRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("ffailed to create new playbook %q role to database: %w", model.RunAdminRoleId, err))
|
|
}
|
|
}
|
|
if _, err := s.Store().Role().GetByName(context.Background(), model.RunMemberRoleId); err != nil {
|
|
if _, err := s.Store().Role().Save(roles[model.RunMemberRoleId]); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new playbook %q role to database: %w", model.RunMemberRoleId, err))
|
|
}
|
|
}
|
|
schemes, err := s.Store().Scheme().GetAllPage(model.SchemeScopeTeam, 0, 1000000)
|
|
if err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to get all schemes: %w", err))
|
|
}
|
|
|
|
for _, scheme := range schemes {
|
|
if scheme.Scope == model.SchemeScopeTeam {
|
|
if scheme.DefaultPlaybookAdminRole == "" {
|
|
playbookAdminRole := &model.Role{
|
|
Name: model.NewId(),
|
|
DisplayName: fmt.Sprintf("Playbook Admin Role for Scheme %s", scheme.Name),
|
|
Permissions: roles[model.PlaybookAdminRoleId].Permissions,
|
|
SchemeManaged: true,
|
|
}
|
|
|
|
if savedRole, err := s.Store().Role().Save(playbookAdminRole); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new playbook %q role for existing custom scheme: %w", model.PlaybookAdminRoleId, err))
|
|
} else {
|
|
scheme.DefaultPlaybookAdminRole = savedRole.Name
|
|
}
|
|
}
|
|
if scheme.DefaultPlaybookMemberRole == "" {
|
|
playbookMember := &model.Role{
|
|
Name: model.NewId(),
|
|
DisplayName: fmt.Sprintf("Playbook Member Role for Scheme %s", scheme.Name),
|
|
Permissions: roles[model.PlaybookMemberRoleId].Permissions,
|
|
SchemeManaged: true,
|
|
}
|
|
|
|
if savedRole, err := s.Store().Role().Save(playbookMember); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new playbook %q role for existing custom scheme: %w", model.PlaybookMemberRoleId, err))
|
|
} else {
|
|
scheme.DefaultPlaybookMemberRole = savedRole.Name
|
|
}
|
|
}
|
|
|
|
if scheme.DefaultRunAdminRole == "" {
|
|
runAdminRole := &model.Role{
|
|
Name: model.NewId(),
|
|
DisplayName: fmt.Sprintf("Run Admin Role for Scheme %s", scheme.Name),
|
|
Permissions: roles[model.RunAdminRoleId].Permissions,
|
|
SchemeManaged: true,
|
|
}
|
|
|
|
if savedRole, err := s.Store().Role().Save(runAdminRole); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new playbook %q role for existing custom scheme: %w", model.RunAdminRoleId, err))
|
|
} else {
|
|
scheme.DefaultRunAdminRole = savedRole.Name
|
|
}
|
|
}
|
|
|
|
if scheme.DefaultRunMemberRole == "" {
|
|
runMemberRole := &model.Role{
|
|
Name: model.NewId(),
|
|
DisplayName: fmt.Sprintf("Run Member Role for Scheme %s", scheme.Name),
|
|
Permissions: roles[model.RunMemberRoleId].Permissions,
|
|
SchemeManaged: true,
|
|
}
|
|
|
|
if savedRole, err := s.Store().Role().Save(runMemberRole); err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to create new playbook %q role for existing custom scheme: %w", model.RunMemberRoleId, err))
|
|
} else {
|
|
scheme.DefaultRunMemberRole = savedRole.Name
|
|
}
|
|
}
|
|
_, err := s.Store().Scheme().Save(scheme)
|
|
if err != nil {
|
|
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to update custom scheme: %w", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
if multiErr != nil {
|
|
return multiErr
|
|
}
|
|
|
|
system := model.System{
|
|
Name: PlaybookRolesCreationMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark playbook roles creation migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doFirstAdminSetupCompleteMigration() error {
|
|
// arbitrary choice, though if there is an longstanding installation with less than 10 messages,
|
|
// putting the first admin through onboarding shouldn't be very disruptive.
|
|
const existingInstallationPostsThreshold = 10
|
|
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(FirstAdminSetupCompleteKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
teams, err := s.Store().Team().GetAll()
|
|
if err != nil {
|
|
// can not confirm that admin has started in this case.
|
|
return fmt.Errorf("could not get teams: %w", err)
|
|
}
|
|
|
|
if len(teams) == 0 {
|
|
// No teams, and no existing preference. This is most likely a new instance.
|
|
// So do not mark that the admin has already done the first time setup.
|
|
return nil
|
|
}
|
|
|
|
// if there are teams, then if this isn't a new installation, there should be posts
|
|
postCount, err := s.Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("could not get posts count from the database: %w", err)
|
|
} else if postCount < existingInstallationPostsThreshold {
|
|
mlog.Info("Post count is lower than expected, aborting migration",
|
|
mlog.Int("expected", int(existingInstallationPostsThreshold)),
|
|
mlog.Int("actual", int(postCount)))
|
|
return nil
|
|
}
|
|
|
|
system := model.System{
|
|
Name: FirstAdminSetupCompleteKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark first admin setup migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doRemainingSchemaMigrations() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(remainingSchemaMigrationsKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
if teams, err := s.Store().Team().GetByEmptyInviteID(); err != nil {
|
|
mlog.Error("Error fetching Teams without InviteID", mlog.Err(err))
|
|
} else {
|
|
for _, team := range teams {
|
|
team.InviteId = model.NewId()
|
|
if _, err := s.Store().Team().Update(team); err != nil {
|
|
return fmt.Errorf("error updating Team InviteIDs %q: %w", team.Id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
system := model.System{
|
|
Name: remainingSchemaMigrationsKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().Save(&system); err != nil {
|
|
return fmt.Errorf("failed to mark the remaining schema migrations as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doPostPriorityConfigDefaultTrueMigration() error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
if _, err := s.Store().System().GetByName(postPriorityConfigDefaultTrueMigrationKey); err == nil {
|
|
return nil
|
|
} else if !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
s.platform.UpdateConfig(func(config *model.Config) {
|
|
config.ServiceSettings.PostPriority = model.NewPointer(true)
|
|
})
|
|
|
|
system := model.System{
|
|
Name: postPriorityConfigDefaultTrueMigrationKey,
|
|
Value: "true",
|
|
}
|
|
|
|
if err := s.Store().System().SaveOrUpdate(&system); err != nil {
|
|
return fmt.Errorf("failed to mark post priority config migration as completed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doSetupContentFlaggingProperties() error {
|
|
// This migration is designed in a way to allow adding more properties in the future.
|
|
// When a new property needs to be added, add it to the expectedPropertiesMap map and
|
|
// update the contentFlaggingMigrationVersion to a new value.
|
|
|
|
// If the migration is already marked as completed, don't do it again.
|
|
var nfErr *store.ErrNotFound
|
|
data, err := s.Store().System().GetByName(contentFlaggingSetupDoneKey)
|
|
if err != nil && !errors.As(err, &nfErr) {
|
|
return fmt.Errorf("could not query migration: %w", err)
|
|
}
|
|
|
|
if data != nil && data.Value == contentFlaggingMigrationVersion {
|
|
return nil
|
|
}
|
|
|
|
// RegisterPropertyGroup is idempotent, so no need to check if group is already registered
|
|
group, err := s.propertyService.RegisterPropertyGroup(model.ContentFlaggingGroupName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to register Content Flagging group: %w", err)
|
|
}
|
|
|
|
// Using page size of 100 and not iterating through all pages because the
|
|
// number of fields are static and defined here and not expected to be more than 100 for now.
|
|
existingProperties, appErr := s.propertyService.SearchPropertyFields(group.ID, model.PropertyFieldSearchOpts{PerPage: 100})
|
|
if appErr != nil {
|
|
return fmt.Errorf("failed to search for existing content flagging properties: %w", appErr)
|
|
}
|
|
|
|
existingPropertiesMap := map[string]*model.PropertyField{}
|
|
for _, property := range existingProperties {
|
|
existingPropertiesMap[property.Name] = property
|
|
}
|
|
|
|
expectedPropertiesMap := map[string]*model.PropertyField{
|
|
contentFlaggingPropertyNameFlaggedPostId: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameFlaggedPostId,
|
|
Type: model.PropertyFieldTypeText,
|
|
},
|
|
ContentFlaggingPropertyNameStatus: {
|
|
GroupID: group.ID,
|
|
Name: ContentFlaggingPropertyNameStatus,
|
|
Type: model.PropertyFieldTypeSelect,
|
|
Attrs: map[string]any{
|
|
"options": []map[string]string{
|
|
{"name": model.ContentFlaggingStatusPending, "color": "light_grey"},
|
|
{"name": model.ContentFlaggingStatusAssigned, "color": "dark_blue"},
|
|
{"name": model.ContentFlaggingStatusRemoved, "color": "dark_red"},
|
|
{"name": model.ContentFlaggingStatusRetained, "color": "light_blue"},
|
|
},
|
|
},
|
|
},
|
|
contentFlaggingPropertyNameReportingUserID: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameReportingUserID,
|
|
Type: model.PropertyFieldTypeUser,
|
|
},
|
|
contentFlaggingPropertyNameReportingReason: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameReportingReason,
|
|
Type: model.PropertyFieldTypeSelect,
|
|
},
|
|
contentFlaggingPropertyNameReportingComment: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameReportingComment,
|
|
Type: model.PropertyFieldTypeText,
|
|
},
|
|
contentFlaggingPropertyNameReportingTime: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameReportingTime,
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: map[string]any{"subType": contentFlaggingPropertySubTypeTimestamp},
|
|
},
|
|
contentFlaggingPropertyNameReviewerUserID: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameReviewerUserID,
|
|
Type: model.PropertyFieldTypeUser,
|
|
Attrs: map[string]any{"editable": true},
|
|
},
|
|
contentFlaggingPropertyNameActorUserID: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameActorUserID,
|
|
Type: model.PropertyFieldTypeUser,
|
|
},
|
|
contentFlaggingPropertyNameActorComment: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameActorComment,
|
|
Type: model.PropertyFieldTypeText,
|
|
},
|
|
contentFlaggingPropertyNameActionTime: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyNameActionTime,
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: map[string]any{"subType": contentFlaggingPropertySubTypeTimestamp},
|
|
},
|
|
contentFlaggingPropertyManageByContentFlagging: {
|
|
GroupID: group.ID,
|
|
Name: contentFlaggingPropertyManageByContentFlagging,
|
|
Type: model.PropertyFieldTypeText,
|
|
},
|
|
}
|
|
|
|
var propertiesToUpdate []*model.PropertyField
|
|
var propertiesToCreate []*model.PropertyField
|
|
|
|
for name, expectedProperty := range expectedPropertiesMap {
|
|
if _, exists := existingPropertiesMap[name]; exists {
|
|
property := existingPropertiesMap[name]
|
|
property.Type = expectedProperty.Type
|
|
property.Attrs = expectedProperty.Attrs
|
|
propertiesToUpdate = append(propertiesToUpdate, property)
|
|
} else {
|
|
propertiesToCreate = append(propertiesToCreate, expectedProperty)
|
|
}
|
|
}
|
|
|
|
for _, property := range propertiesToCreate {
|
|
if _, err := s.propertyService.CreatePropertyField(property); err != nil {
|
|
return fmt.Errorf("failed to create content flagging property: %q, error: %w", property.Name, err)
|
|
}
|
|
}
|
|
|
|
if len(propertiesToUpdate) > 0 {
|
|
if _, err := s.propertyService.UpdatePropertyFields(group.ID, propertiesToUpdate); err != nil {
|
|
return fmt.Errorf("failed to update content flagging property fields: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := s.Store().System().SaveOrUpdate(&model.System{Name: contentFlaggingSetupDoneKey, Value: contentFlaggingMigrationVersion}); err != nil {
|
|
return fmt.Errorf("failed to save content flagging setup done flag in system store %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doCloudS3PathMigrations(rctx request.CTX) error {
|
|
// This migration is only applicable for cloud environments
|
|
if os.Getenv("MM_CLOUD_FILESTORE_BIFROST") == "" {
|
|
return nil
|
|
}
|
|
|
|
// If the migration is already marked as completed, don't do it again.
|
|
if _, err := s.Store().System().GetByName(model.MigrationKeyS3Path); err == nil {
|
|
return nil
|
|
}
|
|
|
|
// If there is a job already pending, no need to schedule again.
|
|
// This is possible if the pod was rolled over.
|
|
jobs, err := s.Store().Job().GetAllByTypeAndStatus(rctx, model.JobTypeS3PathMigration, model.JobStatusPending)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get jobs by type and status: %w", err)
|
|
}
|
|
if len(jobs) > 0 {
|
|
return nil
|
|
}
|
|
|
|
if _, appErr := s.Jobs.CreateJobOnce(rctx, model.JobTypeS3PathMigration, nil); appErr != nil {
|
|
return fmt.Errorf("failed to start job for migrating s3 file paths: %w", appErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doDeleteEmptyDraftsMigration(rctx request.CTX) error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
if _, err := s.Store().System().GetByName(model.MigrationKeyDeleteEmptyDrafts); err == nil {
|
|
return nil
|
|
}
|
|
|
|
jobs, err := s.Store().Job().GetAllByTypeAndStatus(rctx, model.JobTypeDeleteEmptyDraftsMigration, model.JobStatusPending)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get jobs by type and status: %w", err)
|
|
}
|
|
if len(jobs) > 0 {
|
|
return nil
|
|
}
|
|
|
|
if _, appErr := s.Jobs.CreateJobOnce(rctx, model.JobTypeDeleteEmptyDraftsMigration, nil); appErr != nil {
|
|
return fmt.Errorf("failed to start job for deleting empty drafts: %w", appErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doDeleteOrphanDraftsMigration(rctx request.CTX) error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
if _, err := s.Store().System().GetByName(model.MigrationKeyDeleteOrphanDrafts); err == nil {
|
|
return nil
|
|
}
|
|
|
|
jobs, err := s.Store().Job().GetAllByTypeAndStatus(rctx, model.JobTypeDeleteOrphanDraftsMigration, model.JobStatusPending)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get jobs by type and status: %w", err)
|
|
}
|
|
if len(jobs) > 0 {
|
|
return nil
|
|
}
|
|
|
|
if _, appErr := s.Jobs.CreateJobOnce(rctx, model.JobTypeDeleteOrphanDraftsMigration, nil); appErr != nil {
|
|
return fmt.Errorf("failed to start job for deleting orphan drafts: %w", appErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) doDeleteDmsPreferencesMigration(rctx request.CTX) error {
|
|
// If the migration is already marked as completed, don't do it again.
|
|
if _, err := s.Store().System().GetByName(model.MigrationKeyDeleteDmsPreferences); err == nil {
|
|
return nil
|
|
}
|
|
|
|
jobs, err := s.Store().Job().GetAllByTypeAndStatus(rctx, model.JobTypeDeleteDmsPreferencesMigration, model.JobStatusPending)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get jobs by type and status: %w", err)
|
|
}
|
|
if len(jobs) > 0 {
|
|
return nil
|
|
}
|
|
|
|
if _, appErr := s.Jobs.CreateJobOnce(rctx, model.JobTypeDeleteDmsPreferencesMigration, nil); appErr != nil {
|
|
return fmt.Errorf("failed to start job for deleting dm preferences: %w", appErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) DoAppMigrations() {
|
|
a.Srv().doAppMigrations()
|
|
}
|
|
|
|
func (s *Server) doAppMigrations() {
|
|
type migration struct {
|
|
name string
|
|
handler func() error
|
|
}
|
|
m1 := []migration{
|
|
{"Advanced Permissions Migration", s.doAdvancedPermissionsMigration},
|
|
{"Emojis Permissions Migration", s.doEmojisPermissionsMigration},
|
|
{"GuestRolesCreationMigration", s.doGuestRolesCreationMigration},
|
|
{"System Console Roles Creation Migration", s.doSystemConsoleRolesCreationMigration},
|
|
{"Custom Group Admin Role Creation Migration", s.doCustomGroupAdminRoleCreationMigration},
|
|
// This migration always run after dependent migrations such as the guest roles migration.
|
|
{"Permissions Migrations", s.doPermissionsMigrations},
|
|
{"Content Extraction Config Default True Migration", s.doContentExtractionConfigDefaultTrueMigration},
|
|
{"Playbooks Roles Creation Migration", s.doPlaybooksRolesCreationMigration},
|
|
{"First Admin Setup Complete Migration", s.doFirstAdminSetupCompleteMigration},
|
|
{"Remaining Schema Migrations", s.doRemainingSchemaMigrations},
|
|
{"Post Priority Config Default True Migration", s.doPostPriorityConfigDefaultTrueMigration},
|
|
{"Content Flagging Properties Setup", s.doSetupContentFlaggingProperties},
|
|
}
|
|
|
|
for i := range m1 {
|
|
err := m1[i].handler()
|
|
if err != nil {
|
|
mlog.Fatal("Failed to run app migration",
|
|
mlog.String("migration", m1[i].name),
|
|
mlog.Err(err),
|
|
)
|
|
}
|
|
}
|
|
|
|
type migrationContext struct {
|
|
name string
|
|
handler func(request.CTX) error
|
|
}
|
|
m2 := []migrationContext{
|
|
{"Encode S3 Image Paths Migration", s.doCloudS3PathMigrations},
|
|
{"Delete Empty Drafts Migration", s.doDeleteEmptyDraftsMigration},
|
|
{"Delete Orphan Drafts Migration", s.doDeleteOrphanDraftsMigration},
|
|
{"Delete Invalid Dms Preferences Migration", s.doDeleteDmsPreferencesMigration},
|
|
}
|
|
|
|
rctx := request.EmptyContext(s.Log())
|
|
for i := range m2 {
|
|
err := m2[i].handler(rctx)
|
|
if err != nil {
|
|
mlog.Fatal("Failed to run app migration",
|
|
mlog.String("migration", m2[i].name),
|
|
mlog.Err(err),
|
|
)
|
|
}
|
|
}
|
|
}
|