@RestController
@RequestMapping("/api/secure-files")
@Validated
@PreAuthorize("hasRole('USER')")
public class SecureFileController {
private static final Logger logger = LoggerFactory.getLogger(SecureFileController.class);
private final SecureFileService fileService;
private final ApplicationEventPublisher eventPublisher;
public SecureFileController(SecureFileService fileService,
ApplicationEventPublisher eventPublisher) {
this.fileService = fileService;
this.eventPublisher = eventPublisher;
}
// SECURE: Validated backup with allowlisted destinations
@PostMapping("/backup")
public ResponseEntity<BackupResponse> backupFile(
@Valid @RequestBody BackupRequest request,
Authentication auth) {
try {
// Validate user has permission for this operation
if (!hasBackupPermission(auth, request.getFilename())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new BackupResponse(false, "Access denied"));
}
BackupResult result = fileService.performSecureBackup(request);
// Audit logging
eventPublisher.publishEvent(new FileOperationEvent(
"backup", auth.getName(), request.getFilename(), result.isSuccess()
));
return ResponseEntity.ok(new BackupResponse(result.isSuccess(), result.getMessage()));
} catch (ValidationException e) {
logger.warn("Backup validation failed: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(new BackupResponse(false, "Invalid request: " + e.getMessage()));
} catch (SecurityException e) {
logger.error("Security violation in backup: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new BackupResponse(false, "Security violation"));
} catch (Exception e) {
logger.error("Backup operation failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new BackupResponse(false, "Operation failed"));
}
}
// SECURE: Allowlisted operations with validation
@PostMapping("/analyze")
public ResponseEntity<AnalysisResponse> analyzeFile(
@Valid @RequestBody AnalysisRequest request,
Authentication auth) {
try {
AnalysisResult result = fileService.performSecureAnalysis(request);
eventPublisher.publishEvent(new FileOperationEvent(
"analyze", auth.getName(), request.getFilePath(), result.isSuccess()
));
return ResponseEntity.ok(new AnalysisResponse(
result.isSuccess(),
result.getOutput(),
result.getMetadata()
));
} catch (Exception e) {
logger.error("Analysis failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new AnalysisResponse(false, "Analysis failed", null));
}
}
// SECURE: Batch processing with limits and validation
@PostMapping("/batch-process")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<BatchProcessResponse> batchProcess(
@Valid @RequestBody BatchProcessRequest request,
Authentication auth) {
try {
// Validate batch size limits
if (request.getFiles().size() > 10) {
return ResponseEntity.badRequest()
.body(new BatchProcessResponse(false, "Too many files (max 10)", null));
}
BatchProcessResult result = fileService.performBatchProcess(request);
eventPublisher.publishEvent(new FileOperationEvent(
"batch_process", auth.getName(),
String.join(",", request.getFiles()), result.isSuccess()
));
return ResponseEntity.ok(new BatchProcessResponse(
result.isSuccess(),
result.getMessage(),
result.getResults()
));
} catch (Exception e) {
logger.error("Batch processing failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new BatchProcessResponse(false, "Batch processing failed", null));
}
}
@GetMapping("/operations")
public ResponseEntity<List<String>> getAvailableOperations() {
List<String> operations = fileService.getAvailableOperations();
return ResponseEntity.ok(operations);
}
private boolean hasBackupPermission(Authentication auth, String filename) {
// Implement role-based access control
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") ||
a.getAuthority().equals("ROLE_BACKUP_USER"));
}
}
// Validated request classes
class BackupRequest {
@NotBlank(message = "Filename is required")
@Pattern(regexp = "^[a-zA-Z0-9._-]+$", message = "Invalid filename format")
@Size(max = 255, message = "Filename too long")
private String filename;
@ValidBackupDestination
private String destination;
// getters and setters...
}
class AnalysisRequest {
@ValidAnalysisOperation
private String operation;
@ValidFilePath
private String filePath;
@ValidAnalysisOptions
private List<String> options;
// getters and setters...
}
class BatchProcessRequest {
@NotEmpty(message = "File list cannot be empty")
@Size(max = 10, message = "Too many files")
@Valid
private List<@Pattern(regexp = "^[a-zA-Z0-9._-]+$") String> files;
@ValidBatchOperation
private String operation;
// getters and setters...
}
// Custom validators
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = BackupDestinationValidator.class)
public @interface ValidBackupDestination {
String message() default "Invalid backup destination";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Component
public class BackupDestinationValidator implements ConstraintValidator<ValidBackupDestination, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return false;
Set<String> allowedDestinations = Set.of(
"/backup/documents",
"/backup/uploads",
"/backup/temp"
);
return allowedDestinations.contains(value);
}
}
// Response classes
class BackupResponse {
private final boolean success;
private final String message;
public BackupResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
// getters...
}
class AnalysisResponse {
private final boolean success;
private final String output;
private final Map<String, Object> metadata;
public AnalysisResponse(boolean success, String output, Map<String, Object> metadata) {
this.success = success;
this.output = output;
this.metadata = metadata;
}
// getters...
}