// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "encoding/json" "errors" "fmt" "maps" "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/public/utils" "github.com/mattermost/mattermost/server/v8/channels/app/properties" "github.com/mattermost/mattermost/server/v8/channels/store" ) const ( EmojisPermissionsMigrationKey = "EmojisPermissionsMigrationComplete" GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete" SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete" CustomGroupAdminRoleCreationMigrationKey = "CustomGroupAdminRoleCreationMigrationComplete" SharedChannelManagerRoleCreationMigrationKey = "SystemSharedChannelManagerRoleCreationMigrationComplete" ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete" PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete" FirstAdminSetupCompleteKey = model.SystemFirstAdminSetupComplete remainingSchemaMigrationsKey = "RemainingSchemaMigrations" postPriorityConfigDefaultTrueMigrationKey = "PostPriorityConfigDefaultTrueMigrationComplete" contentFlaggingSetupDoneKey = "content_flagging_setup_done" contentFlaggingMigrationVersion = "v5" managedCategorySetupDoneKey = "managed_category_setup_done" managedCategoryMigrationVersion = "v2" boardsPropertySetupDoneKey = "boards_property_setup_done" boardsPropertyMigrationVersion = "v2" cpaDisplayNameBackfillKey = "cpa_display_name_backfill_done" 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(request.EmptyContext(a.Srv().Log())) } func (s *Server) doAdvancedPermissionsMigration(rctx request.CTX) 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(rctx, 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().SaveOrUpdate(&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(request.EmptyContext(a.Srv().Log())); err != nil { return fmt.Errorf("Failed to complete emojis permissions migration: %w", err) } return nil } func (s *Server) doEmojisPermissionsMigration(rctx request.CTX) 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(rctx, 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(rctx, 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().SaveOrUpdate(&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(request.EmptyContext(a.Srv().Log())); err != nil { return fmt.Errorf("Failed to complete guest roles creation migration: %w", err) } return nil } func (s *Server) doGuestRolesCreationMigration(rctx request.CTX) 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(rctx, 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(rctx, 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(rctx, 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().SaveOrUpdate(&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(request.EmptyContext(a.Srv().Log())) } func (s *Server) doSystemConsoleRolesCreationMigration(rctx request.CTX) 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(rctx, 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(rctx, 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(rctx, 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().SaveOrUpdate(&system); err != nil { return fmt.Errorf("failed to mark system console roles creation migration as completed: %w", err) } return nil } func (s *Server) doSingleRoleCreationMigration(rctx request.CTX, migrationKey, roleId string) error { // If the migration is already marked as completed, don't do it again. var nfErr *store.ErrNotFound if _, err := s.Store().System().GetByName(migrationKey); err == nil { return nil } else if !errors.As(err, &nfErr) { return fmt.Errorf("could not query migration: %w", err) } roles := model.MakeDefaultRoles() role := roles[roleId] if role == nil { return fmt.Errorf("unknown role id: %q", roleId) } var nfRoleErr *store.ErrNotFound if _, err := s.Store().Role().GetByName(rctx, roleId); err != nil { if !errors.As(err, &nfRoleErr) { return fmt.Errorf("could not query role %q: %w", roleId, err) } if _, err := s.Store().Role().Save(role); err != nil { return fmt.Errorf("failed to create new role %q: %w", roleId, err) } } system := model.System{ Name: migrationKey, Value: "true", } if err := s.Store().System().SaveOrUpdate(&system); err != nil { return fmt.Errorf("failed to mark %s migration as completed: %w", migrationKey, err) } return nil } func (s *Server) doCustomGroupAdminRoleCreationMigration(rctx request.CTX) error { return s.doSingleRoleCreationMigration(rctx, CustomGroupAdminRoleCreationMigrationKey, model.SystemCustomGroupAdminRoleId) } func (s *Server) doSharedChannelManagerRoleCreationMigration(rctx request.CTX) error { return s.doSingleRoleCreationMigration(rctx, SharedChannelManagerRoleCreationMigrationKey, model.SharedChannelManagerRoleId) } 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 = new(true) }) system := model.System{ Name: ContentExtractionConfigDefaultTrueMigrationKey, Value: "true", } if err := s.Store().System().SaveOrUpdate(&system); err != nil { return fmt.Errorf("failed to mark content extraction config migration as completed: %w", err) } return nil } func (s *Server) doPlaybooksRolesCreationMigration(rctx request.CTX) 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(rctx, 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(rctx, 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(rctx, 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(rctx, 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().SaveOrUpdate(&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().SaveOrUpdate(&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().SaveOrUpdate(&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 = new(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.PropertyGroup{Name: model.ContentFlaggingGroupName, Version: model.PropertyGroupVersionV1}) if err != nil { return fmt.Errorf("failed to register Content Flagging group: %w", err) } rctx := properties.SystemCallerContext(request.EmptyContext(s.Log())) // 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, err := s.propertyService.SearchPropertyFields(rctx, group.ID, model.PropertyFieldSearchOpts{PerPage: 100}) if err != nil { return fmt.Errorf("failed to search for existing content flagging properties: %w", err) } 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(rctx, property); err != nil { // Another server may have won the race and created this field // concurrently (e.g. parallel tests sharing a database pool). // Tolerate that but propagate any other error. if _, retryErr := s.propertyService.GetPropertyFieldByNameForObjectType(rctx, group.ID, "", property.ObjectType, property.Name); retryErr != 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(rctx, group.ID, propertiesToUpdate); err != nil { // Another server may have won the race and updated these fields // concurrently (e.g. parallel tests sharing a database pool). // Both servers write the same expected values, so tolerate the // conflict but propagate any other error. var conflictErr *store.ErrConflict if !errors.As(err, &conflictErr) { 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) doSetupBoardsProperties() error { var nfErr *store.ErrNotFound data, err := s.Store().System().GetByName(boardsPropertySetupDoneKey) if err != nil && !errors.As(err, &nfErr) { return fmt.Errorf("could not query boards migration: %w", err) } if data != nil && data.Value == boardsPropertyMigrationVersion { return nil } group, err := s.propertyService.RegisterPropertyGroup(&model.PropertyGroup{Name: model.BoardsPropertyGroupName, Version: model.PropertyGroupVersionV2}) if err != nil { return fmt.Errorf("failed to register boards property group: %w", err) } rctx := properties.SystemCallerContext(request.EmptyContext(s.Log())) existingProperties, err := s.propertyService.SearchPropertyFields(rctx, group.ID, model.PropertyFieldSearchOpts{PerPage: 100}) if err != nil { return fmt.Errorf("failed to search for existing boards properties: %w", err) } existingPropertiesMap := map[string]*model.PropertyField{} for _, property := range existingProperties { existingPropertiesMap[property.Name] = property } // Default colours seeded by name. Used both when creating the Status field // from scratch and when upgrading an existing v1 install (where we layer // these colours onto the already-persisted options without rewriting their // IDs — see mergeBoardsStatusColors below). statusColorByName := map[string]string{ model.BoardsStatusOptionTodo: model.BoardsStatusColorTodo, model.BoardsStatusOptionInProgress: model.BoardsStatusColorInProgress, model.BoardsStatusOptionComplete: model.BoardsStatusColorComplete, } expectedPropertiesMap := map[string]*model.PropertyField{ model.BoardsPropertyFieldAssignee: { GroupID: group.ID, Name: model.BoardsPropertyFieldAssignee, Type: model.PropertyFieldTypeUser, ObjectType: model.PropertyFieldObjectTypePost, TargetType: string(model.PropertyFieldTargetLevelSystem), Protected: true, PermissionField: model.NewPointer(model.PermissionLevelNone), }, model.BoardsPropertyFieldStatus: { GroupID: group.ID, Name: model.BoardsPropertyFieldStatus, Type: model.PropertyFieldTypeSelect, ObjectType: model.PropertyFieldObjectTypePost, TargetType: string(model.PropertyFieldTargetLevelSystem), Protected: true, PermissionField: model.NewPointer(model.PermissionLevelNone), Attrs: map[string]any{ "options": []map[string]string{ {"name": model.BoardsStatusOptionTodo, "color": model.BoardsStatusColorTodo}, {"name": model.BoardsStatusOptionInProgress, "color": model.BoardsStatusColorInProgress}, {"name": model.BoardsStatusOptionComplete, "color": model.BoardsStatusColorComplete}, }, }, }, } 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.Protected = expectedProperty.Protected property.PermissionField = expectedProperty.PermissionField if name == model.BoardsPropertyFieldStatus { // Status options already exist (with server-generated IDs from the v1 // seed). NewPropertyOptionsFromFieldAttrs regenerates IDs for any // options missing one, so a wholesale attrs replacement would orphan // every reference. Layer colours onto the existing options by name // and preserve their IDs. property.Attrs = mergeBoardsStatusColors(property.Attrs, statusColorByName) } else { property.Attrs = expectedProperty.Attrs } propertiesToUpdate = append(propertiesToUpdate, property) } else { propertiesToCreate = append(propertiesToCreate, expectedProperty) } } for _, property := range propertiesToCreate { if _, err := s.propertyService.CreatePropertyField(rctx, property); err != nil { // Another server may have won the race and created this field // concurrently (e.g. parallel tests sharing a database pool). // Tolerate that but propagate any other error. if _, retryErr := s.propertyService.GetPropertyFieldByNameForObjectType(rctx, group.ID, "", property.ObjectType, property.Name); retryErr != nil { return fmt.Errorf("failed to create boards property: %q, error: %w", property.Name, err) } } } if len(propertiesToUpdate) > 0 { if _, _, _, err := s.propertyService.UpdatePropertyFields(rctx, group.ID, propertiesToUpdate); err != nil { // Another server may have won the race and updated these fields // concurrently (e.g. parallel tests sharing a database pool). // Both servers write the same expected values, so tolerate the // conflict but propagate any other error. var conflictErr *store.ErrConflict if !errors.As(err, &conflictErr) { return fmt.Errorf("failed to update boards property fields: %w", err) } } } if err := s.Store().System().SaveOrUpdate(&model.System{Name: boardsPropertySetupDoneKey, Value: boardsPropertyMigrationVersion}); err != nil { return fmt.Errorf("failed to save boards setup done flag in system store %w", err) } return nil } // mergeBoardsStatusColors layers a name→color map onto the existing options // stored in `attrs`, preserving every existing option's ID. Used during the // v1 → v2 boards migration so previously-seeded Status options gain colours // without having their server-generated IDs regenerated (which would orphan // any card references). Options whose name isn't in `colorByName` are left // untouched; admins who renamed seeded options stay in control. func mergeBoardsStatusColors(attrs model.StringInterface, colorByName map[string]string) model.StringInterface { if attrs == nil { return attrs } rawOptions, ok := attrs["options"] if !ok { return attrs } // attrs is model.StringInterface (map[string]any), so after JSON // deserialisation rawOptions is []any of map[string]any. Round-trip // to []map[string]any so the loop below can index each option by // key without an outer .(map[string]any) assertion per element. encoded, err := json.Marshal(rawOptions) if err != nil { mlog.Warn("Skipping boards Status colour merge: failed to marshal existing options", mlog.Err(err)) return attrs } var options []map[string]any if err := json.Unmarshal(encoded, &options); err != nil { mlog.Warn("Skipping boards Status colour merge: failed to unmarshal existing options", mlog.Err(err)) return attrs } for i, opt := range options { name, _ := opt["name"].(string) if color, found := colorByName[name]; found { options[i]["color"] = color } } out := make(model.StringInterface, len(attrs)) maps.Copy(out, attrs) out["options"] = options return out } // syncSessionAttributeOptions replaces the persisted select options on an // already-seeded session attribute field with the ones the schema declares. // Each existing option's ID is reused by name so IDs stay stable across // restarts, while a field the schema has converted from text to select picks // up its options for the first time. Options are dropped entirely for fields // the schema declares without them, so a select → text conversion doesn't // leave the UI rendering a stale dropdown. func syncSessionAttributeOptions(current, expected *model.PropertyField) error { expectedOptions, ok := expected.Attrs[model.PropertyFieldAttributeOptions] if !ok { delete(current.Attrs, model.PropertyFieldAttributeOptions) return nil } idByName := make(map[string]string) if raw, found := current.Attrs[model.PropertyFieldAttributeOptions]; found { currentOptions, err := model.NewPropertyOptionsFromFieldAttrs[*model.PluginPropertyOption](raw) if err != nil { return fmt.Errorf("failed to read persisted options: %w", err) } for _, option := range currentOptions { idByName[option.GetName()] = option.GetID() } } options, err := model.NewPropertyOptionsFromFieldAttrs[*model.PluginPropertyOption](expectedOptions) if err != nil { return fmt.Errorf("failed to read schema options: %w", err) } // Written back as []any of map[string]any, the canonical attrs shape every // downstream reader of attrs["options"] expects. merged := make([]any, len(options)) for i, option := range options { id := option.GetID() if existingID, found := idByName[option.GetName()]; found { id = existingID } merged[i] = map[string]any{"id": id, "name": option.GetName()} } current.Attrs[model.PropertyFieldAttributeOptions] = merged return nil } // seedSessionAttributeFields idempotently seeds the built-in session attribute property fields. func (s *Server) seedSessionAttributeFields(groupID string) error { rctx := properties.SystemCallerContext(request.EmptyContext(s.Log())) existing, err := s.propertyService.SearchPropertyFields(rctx, groupID, model.PropertyFieldSearchOpts{PerPage: 100}) if err != nil { return fmt.Errorf("failed to search for existing session attribute fields: %w", err) } existingByName := make(map[string]*model.PropertyField, len(existing)) for _, field := range existing { existingByName[field.Name] = field } var fieldsToUpdate []*model.PropertyField var fieldsToCreate []*model.PropertyField for _, expected := range model.SessionAttributeSystemFields(groupID) { if current, ok := existingByName[expected.Name]; ok { current.Type = expected.Type current.Attrs["platforms"] = expected.Attrs["platforms"] current.Attrs[model.SAAttrDisplayName] = expected.Attrs[model.SAAttrDisplayName] if operators, ok := expected.Attrs[model.NativeAttributeAttrOperators]; ok { current.Attrs[model.NativeAttributeAttrOperators] = operators } else { delete(current.Attrs, model.NativeAttributeAttrOperators) } if err := syncSessionAttributeOptions(current, expected); err != nil { return fmt.Errorf("failed to sync options for session attribute field %q: %w", expected.Name, err) } current.ObjectType = expected.ObjectType current.TargetType = expected.TargetType current.Protected = expected.Protected current.PermissionField = expected.PermissionField current.PermissionValues = expected.PermissionValues current.PermissionOptions = expected.PermissionOptions fieldsToUpdate = append(fieldsToUpdate, current) } else { fieldsToCreate = append(fieldsToCreate, expected) } } for _, field := range fieldsToCreate { if _, err := s.propertyService.CreatePropertyField(rctx, field); err != nil { if _, retryErr := s.propertyService.GetPropertyFieldByNameForObjectType(rctx, groupID, "", field.ObjectType, field.Name); retryErr != nil { return fmt.Errorf("failed to create session attribute field: %q, error: %w", field.Name, err) } } } if len(fieldsToUpdate) > 0 { if _, _, _, err := s.propertyService.UpdatePropertyFields(rctx, groupID, fieldsToUpdate); err != nil { var conflictErr *store.ErrConflict if !errors.As(err, &conflictErr) { return fmt.Errorf("failed to update session attribute fields: %w", err) } } } return nil } func (s *Server) doSetupSessionAttributesProperties() error { group, err := s.propertyService.Group(model.SessionAttributesPropertyGroupName) if err != nil { return fmt.Errorf("failed to look up session attributes property group: %w", err) } if err := s.seedSessionAttributeFields(group.ID); err != nil { return fmt.Errorf("failed to seed session attribute fields: %w", err) } return nil } func (s *Server) doSetupManagedCategoryProperties() error { var nfErr *store.ErrNotFound data, err := s.Store().System().GetByName(managedCategorySetupDoneKey) if err != nil && !errors.As(err, &nfErr) { return fmt.Errorf("could not query migration: %w", err) } if data != nil { if data.Value == managedCategoryMigrationVersion { return s.cacheManagedCategoryIDs() } if incrementErr := s.Store().PropertyGroup().IncrementVersion(model.ManagedCategoryPropertyGroupName); incrementErr != nil { return fmt.Errorf("failed to increment managed category group version: %w", incrementErr) } if saveErr := s.Store().System().SaveOrUpdate(&model.System{Name: managedCategorySetupDoneKey, Value: managedCategoryMigrationVersion}); saveErr != nil { return fmt.Errorf("failed to save managed category setup done flag: %w", saveErr) } return s.cacheManagedCategoryIDs() } group, err := s.propertyService.RegisterPropertyGroup(&model.PropertyGroup{Name: model.ManagedCategoryPropertyGroupName, Version: model.PropertyGroupVersionV2}) if err != nil { return fmt.Errorf("failed to register managed category group: %w", err) } rctx := properties.SystemCallerContext(request.EmptyContext(s.Log())) _, err = s.propertyService.GetPropertyFieldByNameForObjectType(rctx, group.ID, "", model.PropertyValueTargetTypeChannel, model.ManagedCategoryPropertyFieldName) if err != nil { field := &model.PropertyField{ GroupID: group.ID, Name: model.ManagedCategoryPropertyFieldName, Type: model.PropertyFieldTypeText, ObjectType: model.PropertyValueTargetTypeChannel, TargetType: "system", TargetID: "", Protected: true, PermissionField: model.NewPointer(model.PermissionLevelNone), PermissionValues: model.NewPointer(model.PermissionLevelMember), PermissionOptions: model.NewPointer(model.PermissionLevelMember), } if _, err := s.propertyService.CreatePropertyField(rctx, field); err != nil { if _, retryErr := s.propertyService.GetPropertyFieldByNameForObjectType(rctx, group.ID, "", field.ObjectType, model.ManagedCategoryPropertyFieldName); retryErr != nil { return fmt.Errorf("failed to create managed category field: %w", err) } } } if err := s.Store().System().SaveOrUpdate(&model.System{Name: managedCategorySetupDoneKey, Value: "true"}); err != nil { return fmt.Errorf("failed to save managed category setup done flag: %w", err) } return s.cacheManagedCategoryIDs() } func (s *Server) doSetupCPADisplayNameBackfill(rctx request.CTX) error { var nfErr *store.ErrNotFound data, err := s.Store().System().GetByName(cpaDisplayNameBackfillKey) if err != nil && !errors.As(err, &nfErr) { return fmt.Errorf("could not query CPA display_name backfill migration: %w", err) } if data != nil { return nil } // The properties package owns the actual field iteration and update logic. // It deliberately bypasses the access-control layer for this single, // well-defined backfill so it can update protected (e.g. UAS-managed) CPA // fields whose source plugin is not the system. Keeping the bypass behind // an explicitly named method on PropertyService avoids exposing a general // "skip access control" surface from this package. backfilled, skipped, err := s.propertyService.MigrateBackfillCPADisplayName(rctx) if err != nil { return fmt.Errorf("failed to backfill CPA display_name: %w", err) } mlog.Info("CPA display_name backfill migration completed", mlog.Int("backfilled", backfilled), mlog.Int("skipped", skipped), ) if err := s.Store().System().SaveOrUpdate(&model.System{ Name: cpaDisplayNameBackfillKey, Value: "true", }); err != nil { return fmt.Errorf("failed to mark CPA display_name backfill as complete: %w", err) } return nil } func (s *Server) cacheManagedCategoryIDs() error { group, err := s.propertyService.GetPropertyGroup(model.ManagedCategoryPropertyGroupName) if err != nil { return fmt.Errorf("failed to get managed category group: %w", err) } rctx := properties.SystemCallerContext(request.EmptyContext(s.Log())) field, err := s.propertyService.GetPropertyFieldByNameForObjectType(rctx, group.ID, "", model.PropertyValueTargetTypeChannel, model.ManagedCategoryPropertyFieldName) if err != nil { return fmt.Errorf("failed to get managed category field: %w", err) } s.Channels().managedCategoryGroupID = group.ID s.Channels().managedCategoryFieldID = field.ID 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 (s *Server) doAccessControlPolicyV0_3Migration(rctx request.CTX) error { var nfErr *store.ErrNotFound if _, err := s.Store().System().GetByName(model.MigrationKeyAccessControlPolicyV0_3); err == nil { return nil } else if !errors.As(err, &nfErr) { return fmt.Errorf("could not query migration: %w", err) } policyTypes := []string{model.AccessControlPolicyTypeParent, model.AccessControlPolicyTypeChannel} const pageSize = 100 for _, policyType := range policyTypes { cursor := model.AccessControlPolicyCursor{} policies, err := utils.Pager(func(_ int) ([]*model.AccessControlPolicy, error) { results, _, err := s.Store().AccessControlPolicy().SearchPolicies(rctx, model.AccessControlPolicySearch{ Type: policyType, Cursor: cursor, Limit: pageSize, }) if err != nil { return nil, err } if len(results) > 0 { cursor = model.AccessControlPolicyCursor{ID: results[len(results)-1].ID} } return results, nil }, pageSize) if err != nil { return fmt.Errorf("failed to search access control policies: %w", err) } for _, policy := range policies { if policy.Version != model.AccessControlPolicyVersionV0_2 { continue } policy.Version = model.AccessControlPolicyVersionV0_3 for i, rule := range policy.Rules { for j, action := range rule.Actions { if action == "*" { policy.Rules[i].Actions[j] = model.AccessControlPolicyActionMembership } } } if _, err := s.Store().AccessControlPolicy().Save(rctx, policy); err != nil { return fmt.Errorf("failed to save migrated access control policy id=%s: %w", policy.ID, err) } if policy.Type == model.AccessControlPolicyTypeChannel { s.Store().Channel().InvalidateChannel(policy.ID) } } } system := model.System{ Name: model.MigrationKeyAccessControlPolicyV0_3, Value: "true", } // SaveOrUpdate is idempotent — another server racing us to run the // same migration (e.g. parallel tests sharing a database pool) would // otherwise trigger a unique-constraint violation on systems_pkey and // mlog.Fatal the process. if err := s.Store().System().SaveOrUpdate(&system); err != nil { return fmt.Errorf("failed to mark access control policy v0.3 migration as completed: %w", err) } return nil } func (a *App) DoAppMigrations() { a.Srv().doAppMigrations() } func (s *Server) doAppMigrations() { rctx := request.EmptyContext(s.Log()) // Migrations read back rows they have just written. Routing those reads to a // replica that has not caught up yet returns no rows, which the mlog.Fatal // calls below turn into an aborted startup. s.Store().LockToMaster() defer s.Store().UnlockFromMaster() type migration struct { name string handler func() error } m1 := []migration{ {"Advanced Permissions Migration", func() error { return s.doAdvancedPermissionsMigration(rctx) }}, {"Emojis Permissions Migration", func() error { return s.doEmojisPermissionsMigration(rctx) }}, {"GuestRolesCreationMigration", func() error { return s.doGuestRolesCreationMigration(rctx) }}, {"System Console Roles Creation Migration", func() error { return s.doSystemConsoleRolesCreationMigration(rctx) }}, {"Custom Group Admin Role Creation Migration", func() error { return s.doCustomGroupAdminRoleCreationMigration(rctx) }}, {"Shared Channel Manager Role Creation Migration", func() error { return s.doSharedChannelManagerRoleCreationMigration(rctx) }}, // 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", func() error { return s.doPlaybooksRolesCreationMigration(rctx) }}, {"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}, {"Boards Properties Setup", s.doSetupBoardsProperties}, {"Managed Category Properties Setup", s.doSetupManagedCategoryProperties}, {"Session Attributes Properties Setup", s.doSetupSessionAttributesProperties}, } 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}, {"Access Control Policy V0.3 Migration", s.doAccessControlPolicyV0_3Migration}, {"CPA DisplayName Backfill", s.doSetupCPADisplayNameBackfill}, } 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), ) } } }