diff --git a/Dockerfile b/Dockerfile index a042ae0cc..719945a7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -92,4 +92,4 @@ EXPOSE 8080/tcp # Set user and run command ENTRYPOINT ["tini", "--", "/scripts/init.sh"] -CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"] \ No newline at end of file +CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] \ No newline at end of file diff --git a/Dockerfile.fat b/Dockerfile.fat index cb02a1cd0..bf1772ff6 100644 --- a/Dockerfile.fat +++ b/Dockerfile.fat @@ -43,7 +43,12 @@ ENV DOCKER_ENABLE_SECURITY=false \ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ UNO_PATH=/usr/lib/libreoffice/program \ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ - PATH=$PATH:/opt/venv/bin + PATH=$PATH:/opt/venv/bin \ + LIBREOFFICE_HOME=/usr/lib/libreoffice \ + SAL_USE_VCLPLUGIN=svp \ + OOO_FORCE_DESKTOP=headless \ + DISPLAY=:99 \ + LD_LIBRARY_PATH=/usr/lib/libreoffice/program # JDK for app @@ -53,6 +58,11 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a apk upgrade --no-cache -a && \ apk add --no-cache \ ca-certificates \ + libxcb libx11 libxtst libxi \ + xvfb-run \ + dbus \ + cairo \ + mesa-dri-gallium \ tzdata \ tini \ bash \ @@ -101,4 +111,4 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a EXPOSE 8080/tcp # Set user and run command ENTRYPOINT ["tini", "--", "/scripts/init.sh"] -CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"] +CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] diff --git a/build.gradle b/build.gradle index 9871d9980..880b276c6 100644 --- a/build.gradle +++ b/build.gradle @@ -429,6 +429,7 @@ dependencies { // Exclude Tomcat and include Jetty implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") + implementation("org.springframework.boot:spring-boot-starter-aop:$springBootVersion") implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion" diff --git a/src/main/java/stirling/software/SPDF/config/security/AopConfig.java b/src/main/java/stirling/software/SPDF/config/security/AopConfig.java new file mode 100644 index 000000000..ed9070a9f --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/AopConfig.java @@ -0,0 +1,10 @@ +package stirling.software.SPDF.config.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@Configuration +@EnableAspectJAutoProxy +public class AopConfig { + // No body needed; annotation-driven +} diff --git a/src/main/java/stirling/software/SPDF/config/security/AuditAspect.java b/src/main/java/stirling/software/SPDF/config/security/AuditAspect.java new file mode 100644 index 000000000..cdb8412f7 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/AuditAspect.java @@ -0,0 +1,91 @@ +package stirling.software.SPDF.config.security; + +import jakarta.servlet.http.HttpServletRequest; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +import java.util.Map; + +@Aspect +@Component +public class AuditAspect { + + @Autowired + private AuditService auditService; + + @Before( + "@annotation(RequestMapping) || @annotation(GetMapping) || @annotation(PostMapping) || " + + "@annotation(PutMapping) || @annotation(DeleteMapping) || @annotation(PatchMapping)" + ) + public void logApiCall(JoinPoint joinPoint) { + try { + // Grab the current request + HttpServletRequest request = + ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + + // Basic info + String method = request.getMethod(); + String endpoint = request.getRequestURI(); + Map requestParams = request.getParameterMap(); + String userAgent = request.getHeader("User-Agent"); + String ipAddress = getClientIp(request); + String username = getCurrentUsername(); + + // Extract file metadata if multipart + MultipartFile[] files = extractFiles(request); + + // Pass to the audit service + auditService.logRequest( + method, + endpoint, + requestParams, + userAgent, + ipAddress, + username, + files + ); + + } catch (Exception e) { + // Log and continue + e.printStackTrace(); + } + } + + private String getClientIp(HttpServletRequest request) { + // Some networks use X-Forwarded-For to pass client IP + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null && !xForwardedFor.isEmpty()) { + return xForwardedFor.split(",")[0]; + } + return request.getRemoteAddr(); + } + + private String getCurrentUsername() { + // If using Spring Security + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.isAuthenticated()) { + return auth.getName(); + } + return "anonymous"; + } + + private MultipartFile[] extractFiles(HttpServletRequest request) { + if (request instanceof MultipartHttpServletRequest multipartRequest) { + Map fileMap = multipartRequest.getFileMap(); + if (!fileMap.isEmpty()) { + return fileMap.values().toArray(new MultipartFile[0]); + } + } + return new MultipartFile[0]; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/AuditService.java b/src/main/java/stirling/software/SPDF/config/security/AuditService.java new file mode 100644 index 000000000..e0c074746 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/AuditService.java @@ -0,0 +1,75 @@ +package stirling.software.SPDF.config.security; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.api.security.AuditLog; +import stirling.software.SPDF.repository.AuditLogRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class AuditService { + + @Autowired + private AuditLogRepository auditLogRepository; + + @Autowired + private ObjectMapper objectMapper; + + public void logRequest( + String method, + String endpoint, + Map requestParams, + String userAgent, + String ipAddress, + String username, + MultipartFile[] files + ) { + try { + // Build the AuditLog entity + AuditLog auditLog = new AuditLog(); + auditLog.setMethod(method); + auditLog.setEndpoint(endpoint); + + // Convert all text form fields (key -> array of values) to JSON for readability + String paramsJson = requestParams != null && !requestParams.isEmpty() + ? objectMapper.writeValueAsString(requestParams) + : null; + auditLog.setRequestParams(paramsJson); + + auditLog.setUserAgent(userAgent); + auditLog.setIpAddress(ipAddress); + auditLog.setUsername(username); + auditLog.setTimestamp(LocalDateTime.now()); + + // Only log metadata for files + if (files != null && files.length > 0) { + String fileNamesStr = Arrays.stream(files) + .map(file -> String.format( + "[name=%s, size=%d, type=%s]", + file.getOriginalFilename(), + file.getSize(), + file.getContentType() + )) + .collect(Collectors.joining("; ")); + auditLog.setFileNames(fileNamesStr); + } + log.info(auditLog.toString()); + // Persist + auditLogRepository.save(auditLog); + + } catch (Exception e) { + // Log error but do not disrupt the main request flow + e.printStackTrace(); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/model/api/security/AuditLog.java b/src/main/java/stirling/software/SPDF/model/api/security/AuditLog.java new file mode 100644 index 000000000..e6c45ddb2 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/api/security/AuditLog.java @@ -0,0 +1,31 @@ +package stirling.software.SPDF.model.api.security; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "audit_logs") +@Data +public class AuditLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String method; + private String endpoint; + + @Lob + private String requestParams; // text form params in JSON + private String userAgent; + private String ipAddress; + private String username; + private LocalDateTime timestamp; + + // Store file metadata (comma or semicolon-separated) + @Lob + private String fileNames; + +} diff --git a/src/main/java/stirling/software/SPDF/repository/AuditLogRepository.java b/src/main/java/stirling/software/SPDF/repository/AuditLogRepository.java new file mode 100644 index 000000000..2c50b656c --- /dev/null +++ b/src/main/java/stirling/software/SPDF/repository/AuditLogRepository.java @@ -0,0 +1,11 @@ +package stirling.software.SPDF.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import stirling.software.SPDF.model.api.security.AuditLog; + +@Repository +public interface AuditLogRepository extends JpaRepository { + // Additional custom queries, if needed +}