mattermost-community-enterp.../account_migration/account_migration.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

277 lines
8.3 KiB
Go

// Copyright (c) 2024 Mattermost Community Enterprise
// Account Migration Implementation
package account_migration
import (
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
// AccountMigrationConfig holds configuration for the account migration interface
type AccountMigrationConfig struct {
Store store.Store
Config func() *model.Config
Logger mlog.LoggerIFace
}
// AccountMigrationImpl implements the AccountMigrationInterface
type AccountMigrationImpl struct {
store store.Store
config func() *model.Config
logger mlog.LoggerIFace
}
// NewAccountMigrationInterface creates a new account migration interface
func NewAccountMigrationInterface(cfg *AccountMigrationConfig) *AccountMigrationImpl {
return &AccountMigrationImpl{
store: cfg.Store,
config: cfg.Config,
logger: cfg.Logger,
}
}
// MigrateToLdap migrates user accounts from one authentication service to LDAP
func (am *AccountMigrationImpl) MigrateToLdap(rctx request.CTX, fromAuthService string, foreignUserFieldNameToMatch string, force bool, dryRun bool) *model.AppError {
cfg := am.config()
// Check if LDAP is enabled
if cfg.LdapSettings.Enable == nil || !*cfg.LdapSettings.Enable {
return model.NewAppError("MigrateToLdap", "account_migration.ldap_not_enabled", nil, "LDAP is not enabled", http.StatusNotImplemented)
}
// Validate fromAuthService
if fromAuthService != model.UserAuthServiceEmail &&
fromAuthService != model.UserAuthServiceSaml &&
fromAuthService != model.ServiceGoogle &&
fromAuthService != model.ServiceOffice365 &&
fromAuthService != model.ServiceOpenid {
return model.NewAppError("MigrateToLdap", "account_migration.invalid_auth_service", map[string]any{"AuthService": fromAuthService}, "Invalid source authentication service", http.StatusBadRequest)
}
// Validate field name to match
validFields := []string{"email", "username", "id"}
validField := false
for _, f := range validFields {
if foreignUserFieldNameToMatch == f {
validField = true
break
}
}
if !validField {
return model.NewAppError("MigrateToLdap", "account_migration.invalid_field", map[string]any{"Field": foreignUserFieldNameToMatch}, "Invalid field name to match", http.StatusBadRequest)
}
am.logger.Info("Starting migration to LDAP",
mlog.String("from_auth_service", fromAuthService),
mlog.String("match_field", foreignUserFieldNameToMatch),
mlog.Bool("force", force),
mlog.Bool("dry_run", dryRun),
)
// Get users with the source auth service
if am.store == nil {
if dryRun {
am.logger.Info("Dry run: Would migrate users from auth service",
mlog.String("from", fromAuthService),
mlog.String("to", model.UserAuthServiceLdap),
)
return nil
}
return model.NewAppError("MigrateToLdap", "account_migration.store_not_available", nil, "Store is not available", http.StatusInternalServerError)
}
// In a real implementation, we would:
// 1. Query users with the source auth service
// 2. For each user, find the matching LDAP entry
// 3. Update the user's auth service and auth data
// 4. If force is true, migrate even if there are conflicts
// 5. If dryRun is true, log what would be done but don't make changes
// Get users to migrate
users, err := am.store.User().GetAllUsingAuthService(fromAuthService)
if err != nil {
return model.NewAppError("MigrateToLdap", "account_migration.get_users_failed", nil, err.Error(), http.StatusInternalServerError)
}
migratedCount := 0
errorCount := 0
for _, user := range users {
// Get the value to match against LDAP
var matchValue string
switch foreignUserFieldNameToMatch {
case "email":
matchValue = user.Email
case "username":
matchValue = user.Username
case "id":
matchValue = user.Id
}
if matchValue == "" {
am.logger.Warn("User has no value for match field",
mlog.String("user_id", user.Id),
mlog.String("field", foreignUserFieldNameToMatch),
)
errorCount++
continue
}
if dryRun {
am.logger.Info("Dry run: Would migrate user to LDAP",
mlog.String("user_id", user.Id),
mlog.String("email", user.Email),
mlog.String("match_value", matchValue),
)
migratedCount++
continue
}
// Update user's auth service to LDAP
user.AuthService = model.UserAuthServiceLdap
user.AuthData = model.NewPointer(matchValue)
if _, updateErr := am.store.User().Update(rctx, user, true); updateErr != nil {
am.logger.Error("Failed to migrate user to LDAP",
mlog.String("user_id", user.Id),
mlog.Err(updateErr),
)
errorCount++
if !force {
continue
}
} else {
migratedCount++
}
}
am.logger.Info("LDAP migration completed",
mlog.Int("migrated", migratedCount),
mlog.Int("errors", errorCount),
mlog.Int("total", len(users)),
mlog.Bool("dry_run", dryRun),
)
return nil
}
// MigrateToSaml migrates user accounts from one authentication service to SAML
func (am *AccountMigrationImpl) MigrateToSaml(rctx request.CTX, fromAuthService string, usersMap map[string]string, auto bool, dryRun bool) *model.AppError {
cfg := am.config()
// Check if SAML is enabled
if cfg.SamlSettings.Enable == nil || !*cfg.SamlSettings.Enable {
return model.NewAppError("MigrateToSaml", "account_migration.saml_not_enabled", nil, "SAML is not enabled", http.StatusNotImplemented)
}
// Validate fromAuthService
if fromAuthService != model.UserAuthServiceEmail &&
fromAuthService != model.UserAuthServiceLdap &&
fromAuthService != model.ServiceGoogle &&
fromAuthService != model.ServiceOffice365 &&
fromAuthService != model.ServiceOpenid {
return model.NewAppError("MigrateToSaml", "account_migration.invalid_auth_service", map[string]any{"AuthService": fromAuthService}, "Invalid source authentication service", http.StatusBadRequest)
}
am.logger.Info("Starting migration to SAML",
mlog.String("from_auth_service", fromAuthService),
mlog.Bool("auto", auto),
mlog.Bool("dry_run", dryRun),
mlog.Int("users_map_count", len(usersMap)),
)
if am.store == nil {
if dryRun {
am.logger.Info("Dry run: Would migrate users from auth service",
mlog.String("from", fromAuthService),
mlog.String("to", model.UserAuthServiceSaml),
)
return nil
}
return model.NewAppError("MigrateToSaml", "account_migration.store_not_available", nil, "Store is not available", http.StatusInternalServerError)
}
var usersToMigrate []*model.User
var err error
if auto {
// Auto migration: get all users with the source auth service
usersToMigrate, err = am.store.User().GetAllUsingAuthService(fromAuthService)
if err != nil {
return model.NewAppError("MigrateToSaml", "account_migration.get_users_failed", nil, err.Error(), http.StatusInternalServerError)
}
} else {
// Manual migration: use the provided user mapping
for userID := range usersMap {
user, err := am.store.User().Get(rctx.Context(), userID)
if err != nil {
am.logger.Warn("User not found",
mlog.String("user_id", userID),
)
continue
}
usersToMigrate = append(usersToMigrate, user)
}
}
migratedCount := 0
errorCount := 0
for _, user := range usersToMigrate {
var samlId string
if auto {
// Auto: use email as SAML ID
samlId = user.Email
} else {
// Manual: use the provided mapping
var ok bool
samlId, ok = usersMap[user.Id]
if !ok {
am.logger.Warn("No SAML ID mapping for user",
mlog.String("user_id", user.Id),
)
errorCount++
continue
}
}
if dryRun {
am.logger.Info("Dry run: Would migrate user to SAML",
mlog.String("user_id", user.Id),
mlog.String("email", user.Email),
mlog.String("saml_id", samlId),
)
migratedCount++
continue
}
// Update user's auth service to SAML
user.AuthService = model.UserAuthServiceSaml
user.AuthData = model.NewPointer(samlId)
if _, updateErr := am.store.User().Update(rctx, user, true); updateErr != nil {
am.logger.Error("Failed to migrate user to SAML",
mlog.String("user_id", user.Id),
mlog.Err(updateErr),
)
errorCount++
} else {
migratedCount++
}
}
am.logger.Info("SAML migration completed",
mlog.Int("migrated", migratedCount),
mlog.Int("errors", errorCount),
mlog.Int("total", len(usersToMigrate)),
mlog.Bool("dry_run", dryRun),
)
return nil
}