348 lines
14 KiB
Go
348 lines
14 KiB
Go
// monorepo/cloud/backend/internal/service/user/complete_deletion.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/gocql/gocql"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
|
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
|
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
|
uc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/collection"
|
|
uc_filemetadata "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata"
|
|
uc_storagedailyusage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/storagedailyusage"
|
|
uc_storageusageevent "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/storageusageevent"
|
|
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
|
)
|
|
|
|
// CompleteUserDeletionRequest represents a GDPR right-to-be-forgotten deletion request
|
|
type CompleteUserDeletionRequest struct {
|
|
UserID gocql.UUID `json:"user_id"`
|
|
Password string `json:"password"` // For authentication
|
|
}
|
|
|
|
// DeletionResult contains comprehensive information about the deletion operation
|
|
type DeletionResult struct {
|
|
UserID gocql.UUID `json:"user_id"`
|
|
FilesDeleted int `json:"files_deleted"`
|
|
CollectionsDeleted int `json:"collections_deleted"`
|
|
S3ObjectsDeleted int `json:"s3_objects_deleted"`
|
|
TotalDataSizeBytes int64 `json:"total_data_size_bytes"`
|
|
MembershipsRemoved int `json:"memberships_removed"`
|
|
DeletedAt time.Time `json:"deleted_at"`
|
|
Success bool `json:"success"`
|
|
Errors []string `json:"errors,omitempty"` // Non-fatal errors
|
|
}
|
|
|
|
// CompleteUserDeletionService orchestrates complete GDPR-compliant user deletion
|
|
type CompleteUserDeletionService interface {
|
|
Execute(ctx context.Context, req *CompleteUserDeletionRequest) (*DeletionResult, error)
|
|
}
|
|
|
|
type completeUserDeletionServiceImpl struct {
|
|
config *config.Configuration
|
|
logger *zap.Logger
|
|
getUserUseCase uc_user.UserGetByIDUseCase
|
|
deleteUserByIDUseCase uc_user.UserDeleteByIDUseCase
|
|
listFilesByOwnerIDService svc_file.ListFilesByOwnerIDService
|
|
softDeleteFileService svc_file.SoftDeleteFileService
|
|
listCollectionsByUserUseCase uc_collection.ListCollectionsByUserUseCase
|
|
softDeleteCollectionService svc_collection.SoftDeleteCollectionService
|
|
removeUserFromAllCollectionsUseCase uc_collection.RemoveUserFromAllCollectionsUseCase
|
|
deleteStorageDailyUsageUseCase uc_storagedailyusage.DeleteByUserUseCase
|
|
deleteStorageUsageEventUseCase uc_storageusageevent.DeleteByUserUseCase
|
|
anonymizeUserIPsImmediatelyUseCase uc_user.AnonymizeUserIPsImmediatelyUseCase
|
|
clearUserCacheUseCase uc_user.ClearUserCacheUseCase
|
|
anonymizeFileUserReferencesUseCase uc_filemetadata.AnonymizeUserReferencesUseCase
|
|
anonymizeCollectionUserReferencesUseCase uc_collection.AnonymizeUserReferencesUseCase
|
|
}
|
|
|
|
func NewCompleteUserDeletionService(
|
|
config *config.Configuration,
|
|
logger *zap.Logger,
|
|
getUserUseCase uc_user.UserGetByIDUseCase,
|
|
deleteUserByIDUseCase uc_user.UserDeleteByIDUseCase,
|
|
listFilesByOwnerIDService svc_file.ListFilesByOwnerIDService,
|
|
softDeleteFileService svc_file.SoftDeleteFileService,
|
|
listCollectionsByUserUseCase uc_collection.ListCollectionsByUserUseCase,
|
|
softDeleteCollectionService svc_collection.SoftDeleteCollectionService,
|
|
removeUserFromAllCollectionsUseCase uc_collection.RemoveUserFromAllCollectionsUseCase,
|
|
deleteStorageDailyUsageUseCase uc_storagedailyusage.DeleteByUserUseCase,
|
|
deleteStorageUsageEventUseCase uc_storageusageevent.DeleteByUserUseCase,
|
|
anonymizeUserIPsImmediatelyUseCase uc_user.AnonymizeUserIPsImmediatelyUseCase,
|
|
clearUserCacheUseCase uc_user.ClearUserCacheUseCase,
|
|
anonymizeFileUserReferencesUseCase uc_filemetadata.AnonymizeUserReferencesUseCase,
|
|
anonymizeCollectionUserReferencesUseCase uc_collection.AnonymizeUserReferencesUseCase,
|
|
) CompleteUserDeletionService {
|
|
logger = logger.Named("CompleteUserDeletionService")
|
|
return &completeUserDeletionServiceImpl{
|
|
config: config,
|
|
logger: logger,
|
|
getUserUseCase: getUserUseCase,
|
|
deleteUserByIDUseCase: deleteUserByIDUseCase,
|
|
listFilesByOwnerIDService: listFilesByOwnerIDService,
|
|
softDeleteFileService: softDeleteFileService,
|
|
listCollectionsByUserUseCase: listCollectionsByUserUseCase,
|
|
softDeleteCollectionService: softDeleteCollectionService,
|
|
removeUserFromAllCollectionsUseCase: removeUserFromAllCollectionsUseCase,
|
|
deleteStorageDailyUsageUseCase: deleteStorageDailyUsageUseCase,
|
|
deleteStorageUsageEventUseCase: deleteStorageUsageEventUseCase,
|
|
anonymizeUserIPsImmediatelyUseCase: anonymizeUserIPsImmediatelyUseCase,
|
|
clearUserCacheUseCase: clearUserCacheUseCase,
|
|
anonymizeFileUserReferencesUseCase: anonymizeFileUserReferencesUseCase,
|
|
anonymizeCollectionUserReferencesUseCase: anonymizeCollectionUserReferencesUseCase,
|
|
}
|
|
}
|
|
|
|
func (svc *completeUserDeletionServiceImpl) Execute(ctx context.Context, req *CompleteUserDeletionRequest) (*DeletionResult, error) {
|
|
//
|
|
// STEP 0: Validation
|
|
//
|
|
if req == nil {
|
|
svc.logger.Warn("Failed validation with nil request")
|
|
return nil, httperror.NewForBadRequestWithSingleField("non_field_error", "Request is required")
|
|
}
|
|
|
|
e := make(map[string]string)
|
|
if req.UserID.String() == "" {
|
|
e["user_id"] = "User ID is required"
|
|
}
|
|
if len(e) != 0 {
|
|
svc.logger.Warn("Failed validating complete user deletion",
|
|
zap.Any("error", e))
|
|
return nil, httperror.NewForBadRequest(&e)
|
|
}
|
|
|
|
result := &DeletionResult{
|
|
UserID: req.UserID,
|
|
DeletedAt: time.Now(),
|
|
Errors: []string{},
|
|
}
|
|
|
|
svc.logger.Info("🚨 Starting GDPR right-to-be-forgotten complete user deletion",
|
|
zap.String("user_id", req.UserID.String()))
|
|
|
|
//
|
|
// STEP 1: Verify user exists
|
|
//
|
|
user, err := svc.getUserUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("User not found for deletion",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
svc.logger.Info("User verified for deletion",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.String("email", user.Email))
|
|
|
|
//
|
|
// STEP 2: List and hard delete all user files
|
|
//
|
|
svc.logger.Info("Step 2/11: Deleting user files...")
|
|
|
|
listFilesReq := &svc_file.ListFilesByOwnerIDRequestDTO{OwnerID: req.UserID}
|
|
filesResp, err := svc.listFilesByOwnerIDService.Execute(ctx, listFilesReq)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to list user files",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("List files: %v", err))
|
|
} else {
|
|
result.FilesDeleted = len(filesResp.Files)
|
|
svc.logger.Info("Found files to delete",
|
|
zap.Int("file_count", result.FilesDeleted))
|
|
|
|
// Hard delete each file (no tombstone - GDPR mode)
|
|
for _, file := range filesResp.Files {
|
|
deleteFileReq := &svc_file.SoftDeleteFileRequestDTO{
|
|
FileID: file.ID,
|
|
ForceHardDelete: true, // GDPR mode - immediate permanent deletion
|
|
}
|
|
deleteResp, err := svc.softDeleteFileService.Execute(ctx, deleteFileReq)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to delete file",
|
|
zap.String("file_id", file.ID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("File %s: %v", file.ID, err))
|
|
} else {
|
|
result.S3ObjectsDeleted++
|
|
result.TotalDataSizeBytes += deleteResp.ReleasedBytes
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// STEP 3: List and hard delete all user collections
|
|
//
|
|
svc.logger.Info("Step 3/11: Deleting user collections...")
|
|
|
|
collections, err := svc.listCollectionsByUserUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to list user collections",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("List collections: %v", err))
|
|
} else {
|
|
result.CollectionsDeleted = len(collections)
|
|
svc.logger.Info("Found collections to delete",
|
|
zap.Int("collection_count", result.CollectionsDeleted))
|
|
|
|
// Hard delete each collection (no tombstone - GDPR mode)
|
|
for _, collection := range collections {
|
|
deleteColReq := &svc_collection.SoftDeleteCollectionRequestDTO{
|
|
ID: collection.ID,
|
|
ForceHardDelete: true, // GDPR mode - immediate permanent deletion
|
|
}
|
|
_, err := svc.softDeleteCollectionService.Execute(ctx, deleteColReq)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to delete collection",
|
|
zap.String("collection_id", collection.ID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Collection %s: %v", collection.ID, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// STEP 4: Remove user from shared collections
|
|
//
|
|
svc.logger.Info("Step 4/11: Removing user from shared collections...")
|
|
|
|
removedCount, err := svc.removeUserFromAllCollectionsUseCase.Execute(ctx, req.UserID, user.Email)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to remove user from shared collections",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Membership cleanup: %v", err))
|
|
} else {
|
|
result.MembershipsRemoved = removedCount
|
|
svc.logger.Info("Removed user from shared collections",
|
|
zap.Int("memberships_removed", removedCount))
|
|
}
|
|
|
|
//
|
|
// STEP 5: Delete storage daily usage data
|
|
//
|
|
svc.logger.Info("Step 5/11: Deleting storage daily usage data...")
|
|
|
|
err = svc.deleteStorageDailyUsageUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to delete storage daily usage",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Storage daily usage: %v", err))
|
|
}
|
|
|
|
//
|
|
// STEP 6: Delete storage usage events
|
|
//
|
|
svc.logger.Info("Step 6/11: Deleting storage usage events...")
|
|
|
|
err = svc.deleteStorageUsageEventUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to delete storage usage events",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Storage usage events: %v", err))
|
|
}
|
|
|
|
//
|
|
// STEP 7: Anonymize all IP addresses
|
|
//
|
|
svc.logger.Info("Step 7/11: Anonymizing IP addresses...")
|
|
|
|
err = svc.anonymizeUserIPsImmediatelyUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to anonymize IP addresses",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("IP anonymization: %v", err))
|
|
}
|
|
|
|
//
|
|
// STEP 8: Anonymize user references in files (CreatedByUserID/ModifiedByUserID)
|
|
//
|
|
svc.logger.Info("Step 8/11: Anonymizing user references in files...")
|
|
|
|
filesUpdated, err := svc.anonymizeFileUserReferencesUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to anonymize user references in files",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("File user references: %v", err))
|
|
} else {
|
|
svc.logger.Info("Anonymized user references in files",
|
|
zap.Int("files_updated", filesUpdated))
|
|
}
|
|
|
|
//
|
|
// STEP 9: Anonymize user references in collections (CreatedByUserID/ModifiedByUserID/GrantedByID)
|
|
//
|
|
svc.logger.Info("Step 9/11: Anonymizing user references in collections...")
|
|
|
|
collectionsUpdated, err := svc.anonymizeCollectionUserReferencesUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to anonymize user references in collections",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Collection user references: %v", err))
|
|
} else {
|
|
svc.logger.Info("Anonymized user references in collections",
|
|
zap.Int("collections_updated", collectionsUpdated))
|
|
}
|
|
|
|
//
|
|
// STEP 10: Clear cache and session data
|
|
//
|
|
svc.logger.Info("Step 10/11: Clearing cache and session data...")
|
|
|
|
err = svc.clearUserCacheUseCase.Execute(ctx, req.UserID, user.Email)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to clear user cache",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
result.Errors = append(result.Errors, fmt.Sprintf("Cache cleanup: %v", err))
|
|
}
|
|
|
|
//
|
|
// STEP 11: Delete user account (final step - point of no return)
|
|
//
|
|
svc.logger.Info("Step 11/11: Deleting user account (final step)...")
|
|
|
|
err = svc.deleteUserByIDUseCase.Execute(ctx, req.UserID)
|
|
if err != nil {
|
|
svc.logger.Error("CRITICAL: User account deletion failed",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.Error(err))
|
|
return nil, fmt.Errorf("CRITICAL: User account deletion failed: %w", err)
|
|
}
|
|
|
|
//
|
|
// SUCCESS
|
|
//
|
|
result.Success = true
|
|
|
|
svc.logger.Info("✅ GDPR right-to-be-forgotten complete user deletion SUCCEEDED",
|
|
zap.String("user_id", req.UserID.String()),
|
|
zap.String("email", user.Email),
|
|
zap.Int("files_deleted", result.FilesDeleted),
|
|
zap.Int("collections_deleted", result.CollectionsDeleted),
|
|
zap.Int("s3_objects_deleted", result.S3ObjectsDeleted),
|
|
zap.Int("memberships_removed", result.MembershipsRemoved),
|
|
zap.Int64("data_size_bytes", result.TotalDataSizeBytes),
|
|
zap.Int("non_fatal_errors", len(result.Errors)))
|
|
|
|
if len(result.Errors) > 0 {
|
|
svc.logger.Warn("Deletion completed with non-fatal errors",
|
|
zap.Strings("errors", result.Errors))
|
|
}
|
|
|
|
return result, nil
|
|
}
|