mattermost-community-enterp.../data_retention/data_retention.go
Claude fad2fe9d3c Initial commit: Mattermost Community Enterprise
Open source implementation of Mattermost Enterprise features:

Authentication & SSO:
- LDAP authentication and sync
- LDAP diagnostics
- SAML 2.0 SSO
- OAuth providers (Google, Office365, OpenID Connect)

Infrastructure:
- Redis-based cluster implementation
- Prometheus metrics
- IP filtering
- Push proxy authentication

Search:
- Bleve search engine (lightweight Elasticsearch alternative)

Compliance & Security:
- Compliance reporting
- Data retention policies
- Message export (Actiance, GlobalRelay, CSV)
- Access control (PAP/PDP)

User Management:
- Account migration (LDAP/SAML)
- ID-loaded push notifications
- Outgoing OAuth connections

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:49:14 +09:00

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
}