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>
454 lines
13 KiB
Go
454 lines
13 KiB
Go
// 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
|
|
}
|