// Copyright (c) 2024 Mattermost Community Enterprise // Data Retention Policy Implementation package data_retention import ( "net/http" "sync" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/store" ) // DataRetentionConfig holds configuration for the data retention interface type DataRetentionConfig struct { Store store.Store Config func() *model.Config Logger mlog.LoggerIFace } // DataRetentionImpl implements the DataRetentionInterface type DataRetentionImpl struct { store store.Store config func() *model.Config logger mlog.LoggerIFace // In-memory storage for policies (in production, this would use the store) policies map[string]*RetentionPolicyData mutex sync.RWMutex } // RetentionPolicyData holds a policy with its associations type RetentionPolicyData struct { Policy model.RetentionPolicy TeamIDs []string ChannelIDs []string } // NewDataRetentionInterface creates a new data retention interface func NewDataRetentionInterface(cfg *DataRetentionConfig) *DataRetentionImpl { return &DataRetentionImpl{ store: cfg.Store, config: cfg.Config, logger: cfg.Logger, policies: make(map[string]*RetentionPolicyData), } } // GetGlobalPolicy returns the global data retention policy func (dr *DataRetentionImpl) GetGlobalPolicy() (*model.GlobalRetentionPolicy, *model.AppError) { cfg := dr.config() if cfg.DataRetentionSettings.EnableMessageDeletion == nil || !*cfg.DataRetentionSettings.EnableMessageDeletion { return &model.GlobalRetentionPolicy{ MessageDeletionEnabled: false, FileDeletionEnabled: false, }, nil } policy := &model.GlobalRetentionPolicy{ MessageDeletionEnabled: true, FileDeletionEnabled: cfg.DataRetentionSettings.EnableFileDeletion != nil && *cfg.DataRetentionSettings.EnableFileDeletion, } // Calculate cutoff times based on retention days if cfg.DataRetentionSettings.MessageRetentionDays != nil { days := *cfg.DataRetentionSettings.MessageRetentionDays policy.MessageRetentionCutoff = time.Now().AddDate(0, 0, -days).UnixMilli() } if cfg.DataRetentionSettings.FileRetentionDays != nil { days := *cfg.DataRetentionSettings.FileRetentionDays policy.FileRetentionCutoff = time.Now().AddDate(0, 0, -days).UnixMilli() } return policy, nil } // GetPolicies returns a list of retention policies with pagination func (dr *DataRetentionImpl) GetPolicies(offset, limit int) (*model.RetentionPolicyWithTeamAndChannelCountsList, *model.AppError) { dr.mutex.RLock() defer dr.mutex.RUnlock() var policies []*model.RetentionPolicyWithTeamAndChannelCounts count := 0 for _, data := range dr.policies { if count >= offset && len(policies) < limit { policies = append(policies, &model.RetentionPolicyWithTeamAndChannelCounts{ RetentionPolicy: data.Policy, TeamCount: int64(len(data.TeamIDs)), ChannelCount: int64(len(data.ChannelIDs)), }) } count++ } return &model.RetentionPolicyWithTeamAndChannelCountsList{ Policies: policies, TotalCount: int64(len(dr.policies)), }, nil } // GetPoliciesCount returns the total count of retention policies func (dr *DataRetentionImpl) GetPoliciesCount() (int64, *model.AppError) { dr.mutex.RLock() defer dr.mutex.RUnlock() return int64(len(dr.policies)), nil } // GetPolicy returns a specific retention policy by ID func (dr *DataRetentionImpl) GetPolicy(policyID string) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) { dr.mutex.RLock() defer dr.mutex.RUnlock() data, ok := dr.policies[policyID] if !ok { return nil, model.NewAppError("GetPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } return &model.RetentionPolicyWithTeamAndChannelCounts{ RetentionPolicy: data.Policy, TeamCount: int64(len(data.TeamIDs)), ChannelCount: int64(len(data.ChannelIDs)), }, nil } // CreatePolicy creates a new retention policy func (dr *DataRetentionImpl) CreatePolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) { dr.mutex.Lock() defer dr.mutex.Unlock() if policy.ID == "" { policy.ID = model.NewId() } if _, exists := dr.policies[policy.ID]; exists { return nil, model.NewAppError("CreatePolicy", "data_retention.policy_exists", map[string]any{"PolicyId": policy.ID}, "", http.StatusConflict) } data := &RetentionPolicyData{ Policy: policy.RetentionPolicy, TeamIDs: policy.TeamIDs, ChannelIDs: policy.ChannelIDs, } dr.policies[policy.ID] = data dr.logger.Info("Created data retention policy", mlog.String("policy_id", policy.ID), mlog.String("display_name", policy.DisplayName), ) return &model.RetentionPolicyWithTeamAndChannelCounts{ RetentionPolicy: data.Policy, TeamCount: int64(len(data.TeamIDs)), ChannelCount: int64(len(data.ChannelIDs)), }, nil } // PatchPolicy updates an existing retention policy func (dr *DataRetentionImpl) PatchPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) { dr.mutex.Lock() defer dr.mutex.Unlock() data, ok := dr.policies[patch.ID] if !ok { return nil, model.NewAppError("PatchPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": patch.ID}, "", http.StatusNotFound) } // Update fields if patch.DisplayName != "" { data.Policy.DisplayName = patch.DisplayName } if patch.PostDurationDays != nil { data.Policy.PostDurationDays = patch.PostDurationDays } if patch.TeamIDs != nil { data.TeamIDs = patch.TeamIDs } if patch.ChannelIDs != nil { data.ChannelIDs = patch.ChannelIDs } dr.logger.Info("Updated data retention policy", mlog.String("policy_id", patch.ID), ) return &model.RetentionPolicyWithTeamAndChannelCounts{ RetentionPolicy: data.Policy, TeamCount: int64(len(data.TeamIDs)), ChannelCount: int64(len(data.ChannelIDs)), }, nil } // DeletePolicy deletes a retention policy func (dr *DataRetentionImpl) DeletePolicy(policyID string) *model.AppError { dr.mutex.Lock() defer dr.mutex.Unlock() if _, ok := dr.policies[policyID]; !ok { return model.NewAppError("DeletePolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } delete(dr.policies, policyID) dr.logger.Info("Deleted data retention policy", mlog.String("policy_id", policyID), ) return nil } // GetTeamsForPolicy returns teams associated with a policy func (dr *DataRetentionImpl) GetTeamsForPolicy(policyID string, offset, limit int) (*model.TeamsWithCount, *model.AppError) { dr.mutex.RLock() defer dr.mutex.RUnlock() data, ok := dr.policies[policyID] if !ok { return nil, model.NewAppError("GetTeamsForPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } // In production, we would fetch actual team data from store var teams []*model.Team end := offset + limit if end > len(data.TeamIDs) { end = len(data.TeamIDs) } for i := offset; i < end; i++ { teams = append(teams, &model.Team{Id: data.TeamIDs[i]}) } return &model.TeamsWithCount{ Teams: teams, TotalCount: int64(len(data.TeamIDs)), }, nil } // AddTeamsToPolicy adds teams to a policy func (dr *DataRetentionImpl) AddTeamsToPolicy(policyID string, teamIDs []string) *model.AppError { dr.mutex.Lock() defer dr.mutex.Unlock() data, ok := dr.policies[policyID] if !ok { return model.NewAppError("AddTeamsToPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } // Add teams (avoiding duplicates) existing := make(map[string]bool) for _, id := range data.TeamIDs { existing[id] = true } for _, id := range teamIDs { if !existing[id] { data.TeamIDs = append(data.TeamIDs, id) } } dr.logger.Info("Added teams to data retention policy", mlog.String("policy_id", policyID), mlog.Int("team_count", len(teamIDs)), ) return nil } // RemoveTeamsFromPolicy removes teams from a policy func (dr *DataRetentionImpl) RemoveTeamsFromPolicy(policyID string, teamIDs []string) *model.AppError { dr.mutex.Lock() defer dr.mutex.Unlock() data, ok := dr.policies[policyID] if !ok { return model.NewAppError("RemoveTeamsFromPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } // Remove teams toRemove := make(map[string]bool) for _, id := range teamIDs { toRemove[id] = true } var remaining []string for _, id := range data.TeamIDs { if !toRemove[id] { remaining = append(remaining, id) } } data.TeamIDs = remaining dr.logger.Info("Removed teams from data retention policy", mlog.String("policy_id", policyID), mlog.Int("team_count", len(teamIDs)), ) return nil } // GetChannelsForPolicy returns channels associated with a policy func (dr *DataRetentionImpl) GetChannelsForPolicy(policyID string, offset, limit int) (*model.ChannelsWithCount, *model.AppError) { dr.mutex.RLock() defer dr.mutex.RUnlock() data, ok := dr.policies[policyID] if !ok { return nil, model.NewAppError("GetChannelsForPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } // In production, we would fetch actual channel data from store var channels model.ChannelListWithTeamData end := offset + limit if end > len(data.ChannelIDs) { end = len(data.ChannelIDs) } for i := offset; i < end; i++ { channels = append(channels, &model.ChannelWithTeamData{ Channel: model.Channel{Id: data.ChannelIDs[i]}, }) } return &model.ChannelsWithCount{ Channels: channels, TotalCount: int64(len(data.ChannelIDs)), }, nil } // AddChannelsToPolicy adds channels to a policy func (dr *DataRetentionImpl) AddChannelsToPolicy(policyID string, channelIDs []string) *model.AppError { dr.mutex.Lock() defer dr.mutex.Unlock() data, ok := dr.policies[policyID] if !ok { return model.NewAppError("AddChannelsToPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } // Add channels (avoiding duplicates) existing := make(map[string]bool) for _, id := range data.ChannelIDs { existing[id] = true } for _, id := range channelIDs { if !existing[id] { data.ChannelIDs = append(data.ChannelIDs, id) } } dr.logger.Info("Added channels to data retention policy", mlog.String("policy_id", policyID), mlog.Int("channel_count", len(channelIDs)), ) return nil } // RemoveChannelsFromPolicy removes channels from a policy func (dr *DataRetentionImpl) RemoveChannelsFromPolicy(policyID string, channelIDs []string) *model.AppError { dr.mutex.Lock() defer dr.mutex.Unlock() data, ok := dr.policies[policyID] if !ok { return model.NewAppError("RemoveChannelsFromPolicy", "data_retention.policy_not_found", map[string]any{"PolicyId": policyID}, "", http.StatusNotFound) } // Remove channels toRemove := make(map[string]bool) for _, id := range channelIDs { toRemove[id] = true } var remaining []string for _, id := range data.ChannelIDs { if !toRemove[id] { remaining = append(remaining, id) } } data.ChannelIDs = remaining dr.logger.Info("Removed channels from data retention policy", mlog.String("policy_id", policyID), mlog.Int("channel_count", len(channelIDs)), ) return nil } // GetTeamPoliciesForUser returns team policies that apply to a user func (dr *DataRetentionImpl) GetTeamPoliciesForUser(userID string, offset, limit int) (*model.RetentionPolicyForTeamList, *model.AppError) { dr.mutex.RLock() defer dr.mutex.RUnlock() // In production, we would query the store to find policies that apply to teams the user is a member of var policies []*model.RetentionPolicyForTeam totalCount := 0 for _, data := range dr.policies { if data.Policy.PostDurationDays != nil { for _, teamID := range data.TeamIDs { if totalCount >= offset && len(policies) < limit { policies = append(policies, &model.RetentionPolicyForTeam{ TeamID: teamID, PostDurationDays: *data.Policy.PostDurationDays, }) } totalCount++ } } } return &model.RetentionPolicyForTeamList{ Policies: policies, TotalCount: int64(totalCount), }, nil } // GetChannelPoliciesForUser returns channel policies that apply to a user func (dr *DataRetentionImpl) GetChannelPoliciesForUser(userID string, offset, limit int) (*model.RetentionPolicyForChannelList, *model.AppError) { dr.mutex.RLock() defer dr.mutex.RUnlock() // In production, we would query the store to find policies that apply to channels the user is a member of var policies []*model.RetentionPolicyForChannel totalCount := 0 for _, data := range dr.policies { if data.Policy.PostDurationDays != nil { for _, channelID := range data.ChannelIDs { if totalCount >= offset && len(policies) < limit { policies = append(policies, &model.RetentionPolicyForChannel{ ChannelID: channelID, PostDurationDays: *data.Policy.PostDurationDays, }) } totalCount++ } } } return &model.RetentionPolicyForChannelList{ Policies: policies, TotalCount: int64(totalCount), }, nil }