Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,348 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue