diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..827325c131c57d542e95ec6f989d83e6ce513afc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM openjdk:21-jdk-slim + +WORKDIR /app + +# Install required packages +RUN apt-get update && apt-get install -y \ + curl \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Copy application files +COPY . . + +# Build application (if build.gradle.kts exists) +RUN if [ -f "build.gradle.kts" ]; then \ + ./gradlew build -x test; \ + fi + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/actuator/health || exit 1 + +# Run application +CMD ["java", "-jar", "build/libs/da-policyengine.jar"] diff --git a/README.md b/README.md index 1141641210ab1a50d90d68d3d4655f92549c7d19..382050ee8ce1ace00f39ec2af88e41d7b0be7bcc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,38 @@ --- -title: Da Policyengine Dev -emoji: ⚡ +title: da-policyengine (dev) +emoji: 🔧 colorFrom: blue -colorTo: red +colorTo: green sdk: docker -pinned: false +app_port: 8080 --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# da-policyengine - dev Environment + +This is the da-policyengine microservice deployed in the dev environment. + +## Features + +- RESTful API endpoints +- Health monitoring via Actuator +- JWT authentication integration +- PostgreSQL database connectivity + +## API Documentation + +Once deployed, API documentation will be available at: +- Swagger UI: https://huggingface.co/spaces/dalabsai/da-policyengine-dev/swagger-ui.html +- Health Check: https://huggingface.co/spaces/dalabsai/da-policyengine-dev/actuator/health + +## Environment + +- **Environment**: dev +- **Port**: 8080 +- **Java Version**: 21 +- **Framework**: Spring Boot + +## Deployment + +This service is automatically deployed via the DALab CI/CD pipeline. + +Last updated: 2025-06-16 23:40:00 diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..abe68a46f3535e75c66f63920bdcf7274f82726e --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,16 @@ +// da-policyengine inherits common configuration from parent build.gradle.kts +// This build file adds policy-engine-specific dependencies + +dependencies { + // Easy Rules engine for policy evaluation + implementation("org.jeasy:easy-rules-core:4.1.0") + implementation("org.jeasy:easy-rules-mvel:4.1.0") + + // Additional dependencies specific to da-policyengine + implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1") +} + +// Configure main application class +configure { + mainClass.set("com.dalab.policyengine.DaPolicyEngineApplication") +} \ No newline at end of file diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4017cae7e25c3fb09d4d288e522bdaa44f30cf95 --- /dev/null +++ b/src/main/docker/Dockerfile @@ -0,0 +1,23 @@ +# Ultra-lean container using Google Distroless +# Expected final size: ~120-180MB (minimal base + JRE + JAR only) + +FROM gcr.io/distroless/java21-debian12:nonroot + +# Set working directory +WORKDIR /app + +# Copy JAR file +COPY build/libs/da-policyengine.jar app.jar + +# Expose standard Spring Boot port +EXPOSE 8080 + +# Run application (distroless has no shell, so use exec form) +ENTRYPOINT ["java", \ + "-XX:+UseContainerSupport", \ + "-XX:MaxRAMPercentage=75.0", \ + "-XX:+UseG1GC", \ + "-XX:+UseStringDeduplication", \ + "-Djava.security.egd=file:/dev/./urandom", \ + "-Dspring.backgroundpreinitializer.ignore=true", \ + "-jar", "app.jar"] diff --git a/src/main/docker/Dockerfile.alpine-jlink b/src/main/docker/Dockerfile.alpine-jlink new file mode 100644 index 0000000000000000000000000000000000000000..ffb0b6cf87260a96fb83ac7a111d2fc905fc3e3b --- /dev/null +++ b/src/main/docker/Dockerfile.alpine-jlink @@ -0,0 +1,43 @@ +# Ultra-minimal Alpine + Custom JRE +# Expected size: ~120-160MB + +# Stage 1: Create custom JRE with only needed modules +FROM eclipse-temurin:21-jdk-alpine as jre-builder +WORKDIR /app + +# Analyze JAR to find required modules +COPY build/libs/*.jar app.jar +RUN jdeps --ignore-missing-deps --print-module-deps app.jar > modules.txt + +# Create minimal JRE with only required modules +RUN jlink \ + --add-modules $(cat modules.txt),java.logging,java.xml,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \ + --strip-debug \ + --no-man-pages \ + --no-header-files \ + --compress=2 \ + --output /custom-jre + +# Stage 2: Production image +FROM alpine:3.19 +RUN apk add --no-cache tzdata && \ + addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +# Copy custom JRE +COPY --from=jre-builder /custom-jre /opt/java +ENV JAVA_HOME=/opt/java +ENV PATH="$JAVA_HOME/bin:$PATH" + +WORKDIR /app +COPY build/libs/*.jar app.jar +RUN chown appuser:appgroup app.jar + +USER appuser +EXPOSE 8080 + +ENTRYPOINT ["java", \ + "-XX:+UseContainerSupport", \ + "-XX:MaxRAMPercentage=70.0", \ + "-XX:+UseG1GC", \ + "-jar", "app.jar"] diff --git a/src/main/docker/Dockerfile.layered b/src/main/docker/Dockerfile.layered new file mode 100644 index 0000000000000000000000000000000000000000..59231523ddffaa9aa5ce1f62b1df71600bae6d7f --- /dev/null +++ b/src/main/docker/Dockerfile.layered @@ -0,0 +1,34 @@ +# Ultra-optimized layered build using Distroless +# Expected size: ~180-220MB with better caching + +FROM gcr.io/distroless/java21-debian12:nonroot as base + +# Stage 1: Extract JAR layers for optimal caching +FROM eclipse-temurin:21-jdk-alpine as extractor +WORKDIR /app +COPY build/libs/*.jar app.jar +RUN java -Djarmode=layertools -jar app.jar extract + +# Stage 2: Production image with extracted layers +FROM base +WORKDIR /app + +# Copy layers in dependency order (best caching) +COPY --from=extractor /app/dependencies/ ./ +COPY --from=extractor /app/spring-boot-loader/ ./ +COPY --from=extractor /app/snapshot-dependencies/ ./ +COPY --from=extractor /app/application/ ./ + +EXPOSE 8080 + +# Optimized JVM settings for micro-containers +ENTRYPOINT ["java", \ + "-XX:+UseContainerSupport", \ + "-XX:MaxRAMPercentage=70.0", \ + "-XX:+UseG1GC", \ + "-XX:+UseStringDeduplication", \ + "-XX:+CompactStrings", \ + "-Xshare:on", \ + "-Djava.security.egd=file:/dev/./urandom", \ + "-Dspring.backgroundpreinitializer.ignore=true", \ + "org.springframework.boot.loader.JarLauncher"] diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native new file mode 100644 index 0000000000000000000000000000000000000000..5135bb6fe05c48308f7b3f756fec66c7525aa6a8 --- /dev/null +++ b/src/main/docker/Dockerfile.native @@ -0,0 +1,20 @@ +# GraalVM Native Image - Ultra-fast startup, tiny size +# Expected size: ~50-80MB, startup <100ms +# Note: Requires native compilation support in Spring Boot + +# Stage 1: Native compilation +FROM ghcr.io/graalvm/graalvm-ce:ol9-java21 as native-builder +WORKDIR /app + +# Install native-image +RUN gu install native-image + +# Copy source and build native executable +COPY . . +RUN ./gradlew nativeCompile + +# Stage 2: Minimal runtime +FROM scratch +COPY --from=native-builder /app/build/native/nativeCompile/app /app +EXPOSE 8080 +ENTRYPOINT ["/app"] diff --git a/src/main/java/com/dalab/policyengine/DaPolicyEngineApplication.java b/src/main/java/com/dalab/policyengine/DaPolicyEngineApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..6f5ec7bda0ae7028210f8015479a2d454c0106d2 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/DaPolicyEngineApplication.java @@ -0,0 +1,37 @@ +package com.dalab.policyengine; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * Main application class for DALab Policy Engine Service. + * + * This service handles: + * - Policy management and evaluation + * - Easy Rules engine integration + * - Kafka event processing for asset changes + * - Policy action execution and notifications + * + * @author DALab Development Team + * @since 1.0.0 + */ +@SpringBootApplication(scanBasePackages = { + "com.dalab.policyengine", + "com.dalab.discovery.common.security" // Include common security utils +}) +@EnableJpaRepositories +@EnableFeignClients +@EnableKafka +@EnableAsync +@EnableScheduling +public class DaPolicyEngineApplication { + + public static void main(String[] args) { + SpringApplication.run(DaPolicyEngineApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/common/ConflictException.java b/src/main/java/com/dalab/policyengine/common/ConflictException.java new file mode 100644 index 0000000000000000000000000000000000000000..1c19819c1f6a5f9fb0d4fba90c0e74b6263d82f4 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/common/ConflictException.java @@ -0,0 +1,11 @@ +package com.dalab.policyengine.common; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.CONFLICT) +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/common/ResourceNotFoundException.java b/src/main/java/com/dalab/policyengine/common/ResourceNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..139732259f9cc327434dacc7ebf720231ce4d5c6 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/common/ResourceNotFoundException.java @@ -0,0 +1,15 @@ +package com.dalab.policyengine.common; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String resourceName, String fieldName, String fieldValue) { + super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); + } + + public ResourceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/config/OpenAPIConfiguration.java b/src/main/java/com/dalab/policyengine/config/OpenAPIConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..f0757bdf0327c602f708b51791c7988ae6e5d41c --- /dev/null +++ b/src/main/java/com/dalab/policyengine/config/OpenAPIConfiguration.java @@ -0,0 +1,27 @@ +package com.dalab.policyengine.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; + +@Configuration +public class OpenAPIConfiguration { + + @Bean + public OpenAPI customOpenAPI( + @Value("${spring.application.name}") String appName, + @Value("${spring.application.description:DALab Policy Engine Microservice}") String appDescription, + @Value("${spring.application.version:v0.0.1}") String appVersion) { + return new OpenAPI() + .info(new Info() + .title(appName) + .version(appVersion) + .description(appDescription) + .termsOfService("http://swagger.io/terms/") // Replace with actual terms + .license(new License().name("Apache 2.0").url("http://springdoc.org"))); // Replace with actual license + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/config/SecurityConfiguration.java b/src/main/java/com/dalab/policyengine/config/SecurityConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..4634dd85159b1cf91ee8619ebfd32928013d6d2b --- /dev/null +++ b/src/main/java/com/dalab/policyengine/config/SecurityConfiguration.java @@ -0,0 +1,44 @@ +package com.dalab.policyengine.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfiguration { + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUri; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // Typically disable CSRF for stateless REST APIs + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authz -> authz + // Define public endpoints if any (e.g., actuator/health, swagger-ui) + // .requestMatchers("/public/**").permitAll() + .anyRequest().authenticated() // All other requests require authentication + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.decoder(jwtDecoder())) + ); + return http.build(); + } + + @Bean + public JwtDecoder jwtDecoder() { + // NimbusJwtDecoder automatically fetches the JWK Set URI from the issuer URI + // (e.g., ISSUER_URI/.well-known/openid-configuration or ISSUER_URI/protocol/openid-connect/certs) + return NimbusJwtDecoder.withIssuerLocation(issuerUri).build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/EventAnalyticsDTO.java b/src/main/java/com/dalab/policyengine/dto/EventAnalyticsDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..08ee1b0bcff86b56b309f84c736f5ecb7b069510 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/EventAnalyticsDTO.java @@ -0,0 +1,349 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import com.dalab.policyengine.model.EventSeverity; +import com.dalab.policyengine.model.EventType; + +/** + * DTO for Event Center analytics and metrics dashboard. + */ +public class EventAnalyticsDTO { + + // Summary metrics + private Long totalEventsToday; + private Long totalEventsThisWeek; + private Long totalEventsThisMonth; + private Long totalActiveSubscriptions; + private Long totalUsersWithSubscriptions; + + // Event distribution by type + private List eventsByType; + + // Event distribution by severity + private List eventsBySeverity; + + // Event distribution by source service + private List eventsBySourceService; + + // Top active subscriptions + private List topActiveSubscriptions; + + // Recent event trends (hourly for last 24 hours) + private List recentTrends; + + // Response time metrics + private ResponseTimeMetrics responseMetrics; + + // Alert statistics + private AlertStatistics alertStats; + + // Time range for this analytics report + private Instant fromTime; + private Instant toTime; + private Instant generatedAt; + + // Constructors + public EventAnalyticsDTO() { + this.generatedAt = Instant.now(); + } + + public EventAnalyticsDTO(Instant fromTime, Instant toTime) { + this.fromTime = fromTime; + this.toTime = toTime; + this.generatedAt = Instant.now(); + } + + // Getters and Setters + + public Long getTotalEventsToday() { + return totalEventsToday; + } + + public void setTotalEventsToday(Long totalEventsToday) { + this.totalEventsToday = totalEventsToday; + } + + public Long getTotalEventsThisWeek() { + return totalEventsThisWeek; + } + + public void setTotalEventsThisWeek(Long totalEventsThisWeek) { + this.totalEventsThisWeek = totalEventsThisWeek; + } + + public Long getTotalEventsThisMonth() { + return totalEventsThisMonth; + } + + public void setTotalEventsThisMonth(Long totalEventsThisMonth) { + this.totalEventsThisMonth = totalEventsThisMonth; + } + + public Long getTotalActiveSubscriptions() { + return totalActiveSubscriptions; + } + + public void setTotalActiveSubscriptions(Long totalActiveSubscriptions) { + this.totalActiveSubscriptions = totalActiveSubscriptions; + } + + public Long getTotalUsersWithSubscriptions() { + return totalUsersWithSubscriptions; + } + + public void setTotalUsersWithSubscriptions(Long totalUsersWithSubscriptions) { + this.totalUsersWithSubscriptions = totalUsersWithSubscriptions; + } + + public List getEventsByType() { + return eventsByType; + } + + public void setEventsByType(List eventsByType) { + this.eventsByType = eventsByType; + } + + public List getEventsBySeverity() { + return eventsBySeverity; + } + + public void setEventsBySeverity(List eventsBySeverity) { + this.eventsBySeverity = eventsBySeverity; + } + + public List getEventsBySourceService() { + return eventsBySourceService; + } + + public void setEventsBySourceService(List eventsBySourceService) { + this.eventsBySourceService = eventsBySourceService; + } + + public List getTopActiveSubscriptions() { + return topActiveSubscriptions; + } + + public void setTopActiveSubscriptions(List topActiveSubscriptions) { + this.topActiveSubscriptions = topActiveSubscriptions; + } + + public List getRecentTrends() { + return recentTrends; + } + + public void setRecentTrends(List recentTrends) { + this.recentTrends = recentTrends; + } + + public ResponseTimeMetrics getResponseMetrics() { + return responseMetrics; + } + + public void setResponseMetrics(ResponseTimeMetrics responseMetrics) { + this.responseMetrics = responseMetrics; + } + + public AlertStatistics getAlertStats() { + return alertStats; + } + + public void setAlertStats(AlertStatistics alertStats) { + this.alertStats = alertStats; + } + + public Instant getFromTime() { + return fromTime; + } + + public void setFromTime(Instant fromTime) { + this.fromTime = fromTime; + } + + public Instant getToTime() { + return toTime; + } + + public void setToTime(Instant toTime) { + this.toTime = toTime; + } + + public Instant getGeneratedAt() { + return generatedAt; + } + + public void setGeneratedAt(Instant generatedAt) { + this.generatedAt = generatedAt; + } + + @Override + public String toString() { + return "EventAnalyticsDTO{" + + "totalEventsToday=" + totalEventsToday + + ", totalEventsThisWeek=" + totalEventsThisWeek + + ", totalEventsThisMonth=" + totalEventsThisMonth + + ", totalActiveSubscriptions=" + totalActiveSubscriptions + + ", fromTime=" + fromTime + + ", toTime=" + toTime + + ", generatedAt=" + generatedAt + + '}'; + } + + // Nested classes for analytics data structures + + public static class EventTypeCount { + private EventType eventType; + private Long count; + private Double percentage; + + public EventTypeCount() {} + + public EventTypeCount(EventType eventType, Long count) { + this.eventType = eventType; + this.count = count; + } + + public EventType getEventType() { return eventType; } + public void setEventType(EventType eventType) { this.eventType = eventType; } + public Long getCount() { return count; } + public void setCount(Long count) { this.count = count; } + public Double getPercentage() { return percentage; } + public void setPercentage(Double percentage) { this.percentage = percentage; } + } + + public static class EventSeverityCount { + private EventSeverity severity; + private Long count; + private Double percentage; + + public EventSeverityCount() {} + + public EventSeverityCount(EventSeverity severity, Long count) { + this.severity = severity; + this.count = count; + } + + public EventSeverity getSeverity() { return severity; } + public void setSeverity(EventSeverity severity) { this.severity = severity; } + public Long getCount() { return count; } + public void setCount(Long count) { this.count = count; } + public Double getPercentage() { return percentage; } + public void setPercentage(Double percentage) { this.percentage = percentage; } + } + + public static class SourceServiceCount { + private String sourceService; + private Long count; + private Double percentage; + + public SourceServiceCount() {} + + public SourceServiceCount(String sourceService, Long count) { + this.sourceService = sourceService; + this.count = count; + } + + public String getSourceService() { return sourceService; } + public void setSourceService(String sourceService) { this.sourceService = sourceService; } + public Long getCount() { return count; } + public void setCount(Long count) { this.count = count; } + public Double getPercentage() { return percentage; } + public void setPercentage(Double percentage) { this.percentage = percentage; } + } + + public static class TopSubscription { + private String subscriptionName; + private Long eventsMatched; + private Long notificationsSent; + private String ownerName; + private Instant lastActivity; + + public TopSubscription() {} + + public TopSubscription(String subscriptionName, Long eventsMatched) { + this.subscriptionName = subscriptionName; + this.eventsMatched = eventsMatched; + } + + public String getSubscriptionName() { return subscriptionName; } + public void setSubscriptionName(String subscriptionName) { this.subscriptionName = subscriptionName; } + public Long getEventsMatched() { return eventsMatched; } + public void setEventsMatched(Long eventsMatched) { this.eventsMatched = eventsMatched; } + public Long getNotificationsSent() { return notificationsSent; } + public void setNotificationsSent(Long notificationsSent) { this.notificationsSent = notificationsSent; } + public String getOwnerName() { return ownerName; } + public void setOwnerName(String ownerName) { this.ownerName = ownerName; } + public Instant getLastActivity() { return lastActivity; } + public void setLastActivity(Instant lastActivity) { this.lastActivity = lastActivity; } + } + + public static class EventTrendPoint { + private Instant timestamp; + private Long eventCount; + private Long criticalCount; + private Long highCount; + private Long mediumCount; + private Long lowCount; + + public EventTrendPoint() {} + + public EventTrendPoint(Instant timestamp, Long eventCount) { + this.timestamp = timestamp; + this.eventCount = eventCount; + } + + public Instant getTimestamp() { return timestamp; } + public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; } + public Long getEventCount() { return eventCount; } + public void setEventCount(Long eventCount) { this.eventCount = eventCount; } + public Long getCriticalCount() { return criticalCount; } + public void setCriticalCount(Long criticalCount) { this.criticalCount = criticalCount; } + public Long getHighCount() { return highCount; } + public void setHighCount(Long highCount) { this.highCount = highCount; } + public Long getMediumCount() { return mediumCount; } + public void setMediumCount(Long mediumCount) { this.mediumCount = mediumCount; } + public Long getLowCount() { return lowCount; } + public void setLowCount(Long lowCount) { this.lowCount = lowCount; } + } + + public static class ResponseTimeMetrics { + private Double averageProcessingTimeMs; + private Double averageNotificationTimeMs; + private Long totalProcessedEvents; + private Long failedNotifications; + private Double successRate; + + public ResponseTimeMetrics() {} + + public Double getAverageProcessingTimeMs() { return averageProcessingTimeMs; } + public void setAverageProcessingTimeMs(Double averageProcessingTimeMs) { this.averageProcessingTimeMs = averageProcessingTimeMs; } + public Double getAverageNotificationTimeMs() { return averageNotificationTimeMs; } + public void setAverageNotificationTimeMs(Double averageNotificationTimeMs) { this.averageNotificationTimeMs = averageNotificationTimeMs; } + public Long getTotalProcessedEvents() { return totalProcessedEvents; } + public void setTotalProcessedEvents(Long totalProcessedEvents) { this.totalProcessedEvents = totalProcessedEvents; } + public Long getFailedNotifications() { return failedNotifications; } + public void setFailedNotifications(Long failedNotifications) { this.failedNotifications = failedNotifications; } + public Double getSuccessRate() { return successRate; } + public void setSuccessRate(Double successRate) { this.successRate = successRate; } + } + + public static class AlertStatistics { + private Long totalAlertsTriggered; + private Long totalNotificationsSent; + private Long totalActionsExecuted; + private Map alertsByChannel; // email, slack, webhook, etc. + + public AlertStatistics() {} + + public Long getTotalAlertsTriggered() { return totalAlertsTriggered; } + public void setTotalAlertsTriggered(Long totalAlertsTriggered) { this.totalAlertsTriggered = totalAlertsTriggered; } + public Long getTotalNotificationsSent() { return totalNotificationsSent; } + public void setTotalNotificationsSent(Long totalNotificationsSent) { this.totalNotificationsSent = totalNotificationsSent; } + public Long getTotalActionsExecuted() { return totalActionsExecuted; } + public void setTotalActionsExecuted(Long totalActionsExecuted) { this.totalActionsExecuted = totalActionsExecuted; } + public Map getAlertsByChannel() { return alertsByChannel; } + public void setAlertsByChannel(Map alertsByChannel) { this.alertsByChannel = alertsByChannel; } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/EventStreamDTO.java b/src/main/java/com/dalab/policyengine/dto/EventStreamDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..dc3d1b903dc39a4d42b556ee84fa8293f36f825f --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/EventStreamDTO.java @@ -0,0 +1,216 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import com.dalab.policyengine.model.EventSeverity; +import com.dalab.policyengine.model.EventType; + +/** + * DTO for streaming events in real-time to the Event Center UI. + */ +public class EventStreamDTO { + + private UUID eventId; + private EventType eventType; + private EventSeverity severity; + private String sourceService; + private String title; + private String description; + private UUID assetId; + private String assetName; + private UUID policyId; + private String policyName; + private Map eventData; + private Map metadata; + private Instant timestamp; + private UUID userId; + private String userAction; + + // Event processing information + private Boolean isProcessed; + private Integer matchingSubscriptionsCount; + private Boolean notificationSent; + private Boolean actionTriggered; + + // Constructors + public EventStreamDTO() {} + + public EventStreamDTO(UUID eventId, EventType eventType, EventSeverity severity, String sourceService) { + this.eventId = eventId; + this.eventType = eventType; + this.severity = severity; + this.sourceService = sourceService; + this.timestamp = Instant.now(); + } + + // Getters and Setters + + public UUID getEventId() { + return eventId; + } + + public void setEventId(UUID eventId) { + this.eventId = eventId; + } + + public EventType getEventType() { + return eventType; + } + + public void setEventType(EventType eventType) { + this.eventType = eventType; + } + + public EventSeverity getSeverity() { + return severity; + } + + public void setSeverity(EventSeverity severity) { + this.severity = severity; + } + + public String getSourceService() { + return sourceService; + } + + public void setSourceService(String sourceService) { + this.sourceService = sourceService; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public UUID getAssetId() { + return assetId; + } + + public void setAssetId(UUID assetId) { + this.assetId = assetId; + } + + public String getAssetName() { + return assetName; + } + + public void setAssetName(String assetName) { + this.assetName = assetName; + } + + public UUID getPolicyId() { + return policyId; + } + + public void setPolicyId(UUID policyId) { + this.policyId = policyId; + } + + public String getPolicyName() { + return policyName; + } + + public void setPolicyName(String policyName) { + this.policyName = policyName; + } + + public Map getEventData() { + return eventData; + } + + public void setEventData(Map eventData) { + this.eventData = eventData; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + public UUID getUserId() { + return userId; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public String getUserAction() { + return userAction; + } + + public void setUserAction(String userAction) { + this.userAction = userAction; + } + + public Boolean getIsProcessed() { + return isProcessed; + } + + public void setIsProcessed(Boolean isProcessed) { + this.isProcessed = isProcessed; + } + + public Integer getMatchingSubscriptionsCount() { + return matchingSubscriptionsCount; + } + + public void setMatchingSubscriptionsCount(Integer matchingSubscriptionsCount) { + this.matchingSubscriptionsCount = matchingSubscriptionsCount; + } + + public Boolean getNotificationSent() { + return notificationSent; + } + + public void setNotificationSent(Boolean notificationSent) { + this.notificationSent = notificationSent; + } + + public Boolean getActionTriggered() { + return actionTriggered; + } + + public void setActionTriggered(Boolean actionTriggered) { + this.actionTriggered = actionTriggered; + } + + @Override + public String toString() { + return "EventStreamDTO{" + + "eventId=" + eventId + + ", eventType=" + eventType + + ", severity=" + severity + + ", sourceService='" + sourceService + '\'' + + ", title='" + title + '\'' + + ", assetId=" + assetId + + ", policyId=" + policyId + + ", timestamp=" + timestamp + + ", isProcessed=" + isProcessed + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/EventSubscriptionInputDTO.java b/src/main/java/com/dalab/policyengine/dto/EventSubscriptionInputDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..d565f8e33bf536ee4d7897c03a115f6793a453da --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/EventSubscriptionInputDTO.java @@ -0,0 +1,229 @@ +package com.dalab.policyengine.dto; + +import java.util.List; +import java.util.Map; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import com.dalab.policyengine.model.EventSeverity; +import com.dalab.policyengine.model.EventType; + +/** + * DTO for creating or updating event subscriptions. + */ +public class EventSubscriptionInputDTO { + + @NotBlank(message = "Subscription name is required") + @Size(max = 255, message = "Subscription name must not exceed 255 characters") + private String name; + + @Size(max = 1000, message = "Description must not exceed 1000 characters") + private String description; + + /** + * Event types to subscribe to + */ + private List eventTypes; + + /** + * Event severity levels to include + */ + private List severities; + + /** + * Source services to monitor + */ + private List sourceServices; + + /** + * Event filtering rules + */ + private List rules; + + /** + * Notification configuration + * Example: { "email": true, "slack": { "channel": "#alerts", "webhook": "https://..." } } + */ + private Map notificationConfig; + + /** + * Action configuration for automated responses + * Example: { "autoQuarantine": true, "escalateTo": "admin", "maxRetries": 3 } + */ + private Map actionConfig; + + // Constructors + public EventSubscriptionInputDTO() {} + + public EventSubscriptionInputDTO(String name) { + this.name = name; + } + + // Getters and Setters + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getEventTypes() { + return eventTypes; + } + + public void setEventTypes(List eventTypes) { + this.eventTypes = eventTypes; + } + + public List getSeverities() { + return severities; + } + + public void setSeverities(List severities) { + this.severities = severities; + } + + public List getSourceServices() { + return sourceServices; + } + + public void setSourceServices(List sourceServices) { + this.sourceServices = sourceServices; + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules; + } + + public Map getNotificationConfig() { + return notificationConfig; + } + + public void setNotificationConfig(Map notificationConfig) { + this.notificationConfig = notificationConfig; + } + + public Map getActionConfig() { + return actionConfig; + } + + public void setActionConfig(Map actionConfig) { + this.actionConfig = actionConfig; + } + + @Override + public String toString() { + return "EventSubscriptionInputDTO{" + + "name='" + name + '\'' + + ", description='" + description + '\'' + + ", eventTypes=" + eventTypes + + ", severities=" + severities + + ", sourceServices=" + sourceServices + + ", rulesCount=" + (rules != null ? rules.size() : 0) + + '}'; + } + + /** + * Nested DTO for event rules within subscription input + */ + public static class EventRuleInputDTO { + @NotBlank(message = "Rule name is required") + @Size(max = 255, message = "Rule name must not exceed 255 characters") + private String name; + + @Size(max = 500, message = "Rule description must not exceed 500 characters") + private String description; + + @NotBlank(message = "Rule condition is required") + private String condition; + + private Integer priority = 1; + + private Boolean enabled = true; + + private Map parameters; + + // Constructors + public EventRuleInputDTO() {} + + public EventRuleInputDTO(String name, String condition) { + this.name = name; + this.condition = condition; + } + + // Getters and Setters + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + @Override + public String toString() { + return "EventRuleInputDTO{" + + "name='" + name + '\'' + + ", condition='" + condition + '\'' + + ", priority=" + priority + + ", enabled=" + enabled + + '}'; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/EventSubscriptionOutputDTO.java b/src/main/java/com/dalab/policyengine/dto/EventSubscriptionOutputDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..6de043ffb6e6c51ef50cee25dc83a8e2884d153d --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/EventSubscriptionOutputDTO.java @@ -0,0 +1,343 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.dalab.policyengine.model.EventSeverity; +import com.dalab.policyengine.model.EventSubscriptionStatus; +import com.dalab.policyengine.model.EventType; + +/** + * DTO for returning event subscription information. + */ +public class EventSubscriptionOutputDTO { + + private UUID id; + private String name; + private String description; + private UUID userId; + private EventSubscriptionStatus status; + private List eventTypes; + private List severities; + private List sourceServices; + private List rules; + private Map notificationConfig; + private Map actionConfig; + private Instant createdAt; + private Instant updatedAt; + private UUID createdByUserId; + private UUID updatedByUserId; + + // Statistics + private Long totalRulesCount; + private Long enabledRulesCount; + private Long eventsMatchedCount; + private Instant lastEventMatchedAt; + + // Constructors + public EventSubscriptionOutputDTO() {} + + public EventSubscriptionOutputDTO(UUID id, String name, UUID userId, EventSubscriptionStatus status) { + this.id = id; + this.name = name; + this.userId = userId; + this.status = status; + } + + // Getters and Setters + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public UUID getUserId() { + return userId; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public EventSubscriptionStatus getStatus() { + return status; + } + + public void setStatus(EventSubscriptionStatus status) { + this.status = status; + } + + public List getEventTypes() { + return eventTypes; + } + + public void setEventTypes(List eventTypes) { + this.eventTypes = eventTypes; + } + + public List getSeverities() { + return severities; + } + + public void setSeverities(List severities) { + this.severities = severities; + } + + public List getSourceServices() { + return sourceServices; + } + + public void setSourceServices(List sourceServices) { + this.sourceServices = sourceServices; + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules; + } + + public Map getNotificationConfig() { + return notificationConfig; + } + + public void setNotificationConfig(Map notificationConfig) { + this.notificationConfig = notificationConfig; + } + + public Map getActionConfig() { + return actionConfig; + } + + public void setActionConfig(Map actionConfig) { + this.actionConfig = actionConfig; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } + + public Long getTotalRulesCount() { + return totalRulesCount; + } + + public void setTotalRulesCount(Long totalRulesCount) { + this.totalRulesCount = totalRulesCount; + } + + public Long getEnabledRulesCount() { + return enabledRulesCount; + } + + public void setEnabledRulesCount(Long enabledRulesCount) { + this.enabledRulesCount = enabledRulesCount; + } + + public Long getEventsMatchedCount() { + return eventsMatchedCount; + } + + public void setEventsMatchedCount(Long eventsMatchedCount) { + this.eventsMatchedCount = eventsMatchedCount; + } + + public Instant getLastEventMatchedAt() { + return lastEventMatchedAt; + } + + public void setLastEventMatchedAt(Instant lastEventMatchedAt) { + this.lastEventMatchedAt = lastEventMatchedAt; + } + + @Override + public String toString() { + return "EventSubscriptionOutputDTO{" + + "id=" + id + + ", name='" + name + '\'' + + ", userId=" + userId + + ", status=" + status + + ", eventTypes=" + eventTypes + + ", severities=" + severities + + ", rulesCount=" + totalRulesCount + + ", eventsMatchedCount=" + eventsMatchedCount + + '}'; + } + + /** + * Nested DTO for event rules within subscription output + */ + public static class EventRuleOutputDTO { + private UUID id; + private String name; + private String description; + private String condition; + private Integer priority; + private Boolean enabled; + private Map parameters; + private Instant createdAt; + private Instant updatedAt; + private UUID createdByUserId; + private UUID updatedByUserId; + + // Constructors + public EventRuleOutputDTO() {} + + public EventRuleOutputDTO(UUID id, String name, String condition) { + this.id = id; + this.name = name; + this.condition = condition; + } + + // Getters and Setters + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } + + @Override + public String toString() { + return "EventRuleOutputDTO{" + + "id=" + id + + ", name='" + name + '\'' + + ", condition='" + condition + '\'' + + ", priority=" + priority + + ", enabled=" + enabled + + '}'; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyDraftActionDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyDraftActionDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..113cad153305bb7ab5a6af1f35c5d5b19338d0e7 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyDraftActionDTO.java @@ -0,0 +1,268 @@ +package com.dalab.policyengine.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * DTO for policy draft workflow actions (submit, approve, reject, etc.). + * Used for all workflow transition operations. + */ +public class PolicyDraftActionDTO { + + /** + * The action to perform (SUBMIT, APPROVE, REJECT, REQUEST_CHANGES, ARCHIVE). + */ + @NotBlank(message = "Action type is required") + private String actionType; + + /** + * Comment or reason for the action (required for rejection and optional for others). + */ + @Size(max = 2000, message = "Comment must not exceed 2000 characters") + private String comment; + + /** + * Priority level if submitting or approving (HIGH, MEDIUM, LOW). + */ + private String priority; + + /** + * Target implementation date if approving for publication. + */ + private String targetImplementationDate; + + /** + * Additional metadata for the action. + */ + private String metadata; + + // Constructors + public PolicyDraftActionDTO() {} + + public PolicyDraftActionDTO(String actionType) { + this.actionType = actionType; + } + + public PolicyDraftActionDTO(String actionType, String comment) { + this.actionType = actionType; + this.comment = comment; + } + + // Getters and Setters + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getTargetImplementationDate() { + return targetImplementationDate; + } + + public void setTargetImplementationDate(String targetImplementationDate) { + this.targetImplementationDate = targetImplementationDate; + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } +} + +/** + * DTO for bulk operations on multiple policy drafts. + */ +class PolicyDraftBulkActionDTO { + + /** + * List of draft IDs to perform action on. + */ + @NotBlank(message = "Draft IDs are required") + private java.util.List draftIds; + + /** + * The action to perform on all selected drafts. + */ + @NotBlank(message = "Action type is required") + private String actionType; + + /** + * Comment for the bulk action. + */ + @Size(max = 1000, message = "Comment must not exceed 1000 characters") + private String comment; + + // Constructors + public PolicyDraftBulkActionDTO() {} + + // Getters and Setters + public java.util.List getDraftIds() { + return draftIds; + } + + public void setDraftIds(java.util.List draftIds) { + this.draftIds = draftIds; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} + +/** + * DTO for policy draft summary information (for list views). + */ +class PolicyDraftSummaryDTO { + + private java.util.UUID id; + private String name; + private String status; + private Integer version; + private String priority; + private String category; + private java.time.Instant createdAt; + private java.time.Instant updatedAt; + private java.util.UUID createdByUserId; + private java.time.Instant targetImplementationDate; + private int reviewCommentsCount; + private boolean isOverdue; + + // Constructors + public PolicyDraftSummaryDTO() {} + + public PolicyDraftSummaryDTO(java.util.UUID id, String name, String status) { + this.id = id; + this.name = name; + this.status = status; + } + + // Getters and Setters + public java.util.UUID getId() { + return id; + } + + public void setId(java.util.UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public java.time.Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(java.time.Instant createdAt) { + this.createdAt = createdAt; + } + + public java.time.Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(java.time.Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public java.util.UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(java.util.UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public java.time.Instant getTargetImplementationDate() { + return targetImplementationDate; + } + + public void setTargetImplementationDate(java.time.Instant targetImplementationDate) { + this.targetImplementationDate = targetImplementationDate; + } + + public int getReviewCommentsCount() { + return reviewCommentsCount; + } + + public void setReviewCommentsCount(int reviewCommentsCount) { + this.reviewCommentsCount = reviewCommentsCount; + } + + public boolean isOverdue() { + return isOverdue; + } + + public void setOverdue(boolean overdue) { + isOverdue = overdue; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyDraftInputDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyDraftInputDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..b78d666e4d688ecca4abf009718f1297b4de61ad --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyDraftInputDTO.java @@ -0,0 +1,227 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * DTO for creating and updating policy drafts. + * Contains all information needed for draft management and workflow. + */ +public class PolicyDraftInputDTO { + + /** + * Name of the policy (must be unique when published). + */ + @NotBlank(message = "Policy name is required") + @Size(max = 255, message = "Policy name must not exceed 255 characters") + private String name; + + /** + * Detailed description of the policy's purpose and scope. + */ + @Size(max = 2000, message = "Description must not exceed 2000 characters") + private String description; + + /** + * Reference to the published policy if this is an update. + */ + private UUID basePolicyId; + + /** + * MVEL condition logic for the policy evaluation. + */ + private String conditionLogic; + + /** + * JSON representation of policy rules structure. + */ + private List> rulesDefinition; + + /** + * JSON representation of actions to be taken when policy is triggered. + */ + private Map actions; + + /** + * Change summary describing what was modified in this version. + */ + @Size(max = 1000, message = "Change summary must not exceed 1000 characters") + private String changeSummary; + + /** + * Justification for the policy changes or creation. + */ + @Size(max = 2000, message = "Justification must not exceed 2000 characters") + private String justification; + + /** + * Expected impact of implementing this policy. + */ + @Size(max = 1000, message = "Expected impact must not exceed 1000 characters") + private String expectedImpact; + + /** + * Target implementation date for the policy. + */ + private Instant targetImplementationDate; + + /** + * Priority level for policy implementation. + */ + private String priority = "MEDIUM"; + + /** + * Business category or domain this policy applies to. + */ + @Size(max = 100, message = "Category must not exceed 100 characters") + private String category; + + /** + * Tags for categorization and searchability. + */ + private List tags; + + /** + * Stakeholders who should be notified about this policy. + */ + private List stakeholders; + + /** + * Additional metadata for workflow management. + */ + private Map workflowMetadata; + + // Constructors + public PolicyDraftInputDTO() {} + + public PolicyDraftInputDTO(String name, String description) { + this.name = name; + this.description = description; + } + + // Getters and Setters + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public UUID getBasePolicyId() { + return basePolicyId; + } + + public void setBasePolicyId(UUID basePolicyId) { + this.basePolicyId = basePolicyId; + } + + public String getConditionLogic() { + return conditionLogic; + } + + public void setConditionLogic(String conditionLogic) { + this.conditionLogic = conditionLogic; + } + + public List> getRulesDefinition() { + return rulesDefinition; + } + + public void setRulesDefinition(List> rulesDefinition) { + this.rulesDefinition = rulesDefinition; + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } + + public String getChangeSummary() { + return changeSummary; + } + + public void setChangeSummary(String changeSummary) { + this.changeSummary = changeSummary; + } + + public String getJustification() { + return justification; + } + + public void setJustification(String justification) { + this.justification = justification; + } + + public String getExpectedImpact() { + return expectedImpact; + } + + public void setExpectedImpact(String expectedImpact) { + this.expectedImpact = expectedImpact; + } + + public Instant getTargetImplementationDate() { + return targetImplementationDate; + } + + public void setTargetImplementationDate(Instant targetImplementationDate) { + this.targetImplementationDate = targetImplementationDate; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public List getStakeholders() { + return stakeholders; + } + + public void setStakeholders(List stakeholders) { + this.stakeholders = stakeholders; + } + + public Map getWorkflowMetadata() { + return workflowMetadata; + } + + public void setWorkflowMetadata(Map workflowMetadata) { + this.workflowMetadata = workflowMetadata; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyDraftOutputDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyDraftOutputDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..a83370796cb3c005450d6f100f34071647d7e5a8 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyDraftOutputDTO.java @@ -0,0 +1,412 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * DTO for returning policy draft information including workflow status and audit trail. + * Contains comprehensive information for UI display and workflow management. + */ +public class PolicyDraftOutputDTO { + + private UUID id; + private String name; + private String description; + private String status; + private Integer version; + private UUID basePolicyId; + private String conditionLogic; + private List> rulesDefinition; + private Map actions; + private String changeSummary; + private String justification; + private String expectedImpact; + private Instant targetImplementationDate; + private String priority; + private String category; + private List tags; + private List stakeholders; + private Map approvalMetadata; + private List> reviewComments; + + // Audit trail information + private Instant createdAt; + private Instant updatedAt; + private UUID createdByUserId; + private UUID updatedByUserId; + private Instant submittedAt; + private UUID submittedByUserId; + private Instant approvedAt; + private UUID approvedByUserId; + private Instant rejectedAt; + private UUID rejectedByUserId; + private Instant publishedAt; + private UUID publishedByUserId; + + // Enhanced workflow information + private WorkflowStatusDTO workflowStatus; + private List availableActions; + + // Constructors + public PolicyDraftOutputDTO() {} + + public PolicyDraftOutputDTO(UUID id, String name, String status) { + this.id = id; + this.name = name; + this.status = status; + } + + // Getters and Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public UUID getBasePolicyId() { + return basePolicyId; + } + + public void setBasePolicyId(UUID basePolicyId) { + this.basePolicyId = basePolicyId; + } + + public String getConditionLogic() { + return conditionLogic; + } + + public void setConditionLogic(String conditionLogic) { + this.conditionLogic = conditionLogic; + } + + public List> getRulesDefinition() { + return rulesDefinition; + } + + public void setRulesDefinition(List> rulesDefinition) { + this.rulesDefinition = rulesDefinition; + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } + + public String getChangeSummary() { + return changeSummary; + } + + public void setChangeSummary(String changeSummary) { + this.changeSummary = changeSummary; + } + + public String getJustification() { + return justification; + } + + public void setJustification(String justification) { + this.justification = justification; + } + + public String getExpectedImpact() { + return expectedImpact; + } + + public void setExpectedImpact(String expectedImpact) { + this.expectedImpact = expectedImpact; + } + + public Instant getTargetImplementationDate() { + return targetImplementationDate; + } + + public void setTargetImplementationDate(Instant targetImplementationDate) { + this.targetImplementationDate = targetImplementationDate; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public List getStakeholders() { + return stakeholders; + } + + public void setStakeholders(List stakeholders) { + this.stakeholders = stakeholders; + } + + public Map getApprovalMetadata() { + return approvalMetadata; + } + + public void setApprovalMetadata(Map approvalMetadata) { + this.approvalMetadata = approvalMetadata; + } + + public List> getReviewComments() { + return reviewComments; + } + + public void setReviewComments(List> reviewComments) { + this.reviewComments = reviewComments; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } + + public Instant getSubmittedAt() { + return submittedAt; + } + + public void setSubmittedAt(Instant submittedAt) { + this.submittedAt = submittedAt; + } + + public UUID getSubmittedByUserId() { + return submittedByUserId; + } + + public void setSubmittedByUserId(UUID submittedByUserId) { + this.submittedByUserId = submittedByUserId; + } + + public Instant getApprovedAt() { + return approvedAt; + } + + public void setApprovedAt(Instant approvedAt) { + this.approvedAt = approvedAt; + } + + public UUID getApprovedByUserId() { + return approvedByUserId; + } + + public void setApprovedByUserId(UUID approvedByUserId) { + this.approvedByUserId = approvedByUserId; + } + + public Instant getRejectedAt() { + return rejectedAt; + } + + public void setRejectedAt(Instant rejectedAt) { + this.rejectedAt = rejectedAt; + } + + public UUID getRejectedByUserId() { + return rejectedByUserId; + } + + public void setRejectedByUserId(UUID rejectedByUserId) { + this.rejectedByUserId = rejectedByUserId; + } + + public Instant getPublishedAt() { + return publishedAt; + } + + public void setPublishedAt(Instant publishedAt) { + this.publishedAt = publishedAt; + } + + public UUID getPublishedByUserId() { + return publishedByUserId; + } + + public void setPublishedByUserId(UUID publishedByUserId) { + this.publishedByUserId = publishedByUserId; + } + + public WorkflowStatusDTO getWorkflowStatus() { + return workflowStatus; + } + + public void setWorkflowStatus(WorkflowStatusDTO workflowStatus) { + this.workflowStatus = workflowStatus; + } + + public List getAvailableActions() { + return availableActions; + } + + public void setAvailableActions(List availableActions) { + this.availableActions = availableActions; + } + + /** + * Nested DTO for workflow status information. + */ + public static class WorkflowStatusDTO { + private String currentStage; + private String statusDescription; + private List nextPossibleStates; + private boolean canEdit; + private boolean canSubmit; + private boolean canApprove; + private boolean canReject; + private boolean canPublish; + private String stageColor; // For UI status indicators + private Integer daysInCurrentStatus; + + // Constructors, getters, and setters + public WorkflowStatusDTO() {} + + public String getCurrentStage() { return currentStage; } + public void setCurrentStage(String currentStage) { this.currentStage = currentStage; } + + public String getStatusDescription() { return statusDescription; } + public void setStatusDescription(String statusDescription) { this.statusDescription = statusDescription; } + + public List getNextPossibleStates() { return nextPossibleStates; } + public void setNextPossibleStates(List nextPossibleStates) { this.nextPossibleStates = nextPossibleStates; } + + public boolean isCanEdit() { return canEdit; } + public void setCanEdit(boolean canEdit) { this.canEdit = canEdit; } + + public boolean isCanSubmit() { return canSubmit; } + public void setCanSubmit(boolean canSubmit) { this.canSubmit = canSubmit; } + + public boolean isCanApprove() { return canApprove; } + public void setCanApprove(boolean canApprove) { this.canApprove = canApprove; } + + public boolean isCanReject() { return canReject; } + public void setCanReject(boolean canReject) { this.canReject = canReject; } + + public boolean isCanPublish() { return canPublish; } + public void setCanPublish(boolean canPublish) { this.canPublish = canPublish; } + + public String getStageColor() { return stageColor; } + public void setStageColor(String stageColor) { this.stageColor = stageColor; } + + public Integer getDaysInCurrentStatus() { return daysInCurrentStatus; } + public void setDaysInCurrentStatus(Integer daysInCurrentStatus) { this.daysInCurrentStatus = daysInCurrentStatus; } + } + + /** + * Nested DTO for available workflow actions. + */ + public static class WorkflowActionDTO { + private String actionType; + private String actionLabel; + private String actionDescription; + private boolean requiresComment; + private String confirmationMessage; + private String buttonStyle; // For UI styling + + // Constructors, getters, and setters + public WorkflowActionDTO() {} + + public WorkflowActionDTO(String actionType, String actionLabel) { + this.actionType = actionType; + this.actionLabel = actionLabel; + } + + public String getActionType() { return actionType; } + public void setActionType(String actionType) { this.actionType = actionType; } + + public String getActionLabel() { return actionLabel; } + public void setActionLabel(String actionLabel) { this.actionLabel = actionLabel; } + + public String getActionDescription() { return actionDescription; } + public void setActionDescription(String actionDescription) { this.actionDescription = actionDescription; } + + public boolean isRequiresComment() { return requiresComment; } + public void setRequiresComment(boolean requiresComment) { this.requiresComment = requiresComment; } + + public String getConfirmationMessage() { return confirmationMessage; } + public void setConfirmationMessage(String confirmationMessage) { this.confirmationMessage = confirmationMessage; } + + public String getButtonStyle() { return buttonStyle; } + public void setButtonStyle(String buttonStyle) { this.buttonStyle = buttonStyle; } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationOutputDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationOutputDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..8e91f8262c7fc4a545ca5b684772d4fe2f8b17a6 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationOutputDTO.java @@ -0,0 +1,92 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import com.dalab.policyengine.model.PolicyEvaluationStatus; + +public class PolicyEvaluationOutputDTO { + private UUID id; + private UUID policyId; + private String policyName; // For convenience + private String targetAssetId; + private PolicyEvaluationStatus status; + private Map evaluationDetails; // e.g., facts used, rules evaluated, outcome per rule + private Map triggeredActions; // Actions taken based on the policy + private Instant evaluatedAt; + private UUID evaluationTriggeredByUserId; + + // Getters and Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getPolicyId() { + return policyId; + } + + public void setPolicyId(UUID policyId) { + this.policyId = policyId; + } + + public String getPolicyName() { + return policyName; + } + + public void setPolicyName(String policyName) { + this.policyName = policyName; + } + + public String getTargetAssetId() { + return targetAssetId; + } + + public void setTargetAssetId(String targetAssetId) { + this.targetAssetId = targetAssetId; + } + + public PolicyEvaluationStatus getStatus() { + return status; + } + + public void setStatus(PolicyEvaluationStatus status) { + this.status = status; + } + + public Map getEvaluationDetails() { + return evaluationDetails; + } + + public void setEvaluationDetails(Map evaluationDetails) { + this.evaluationDetails = evaluationDetails; + } + + public Map getTriggeredActions() { + return triggeredActions; + } + + public void setTriggeredActions(Map triggeredActions) { + this.triggeredActions = triggeredActions; + } + + public Instant getEvaluatedAt() { + return evaluatedAt; + } + + public void setEvaluatedAt(Instant evaluatedAt) { + this.evaluatedAt = evaluatedAt; + } + + public UUID getEvaluationTriggeredByUserId() { + return evaluationTriggeredByUserId; + } + + public void setEvaluationTriggeredByUserId(UUID evaluationTriggeredByUserId) { + this.evaluationTriggeredByUserId = evaluationTriggeredByUserId; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationRequestDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..bbd35538c9bf548ea9f850550091c8c19b999919 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationRequestDTO.java @@ -0,0 +1,32 @@ +package com.dalab.policyengine.dto; + +import java.util.Map; + +import jakarta.validation.constraints.NotBlank; + +public class PolicyEvaluationRequestDTO { + + @NotBlank + private String targetAssetId; // The ID of the asset to be evaluated (e.g., from da-catalog) + + // Optional: Additional context or facts that might not be directly part of the asset + // but are relevant for this specific evaluation. + private Map evaluationContext; + + // Getters and Setters + public String getTargetAssetId() { + return targetAssetId; + } + + public void setTargetAssetId(String targetAssetId) { + this.targetAssetId = targetAssetId; + } + + public Map getEvaluationContext() { + return evaluationContext; + } + + public void setEvaluationContext(Map evaluationContext) { + this.evaluationContext = evaluationContext; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationSummaryDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationSummaryDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..1603ef6a1cc2aaa66bf9b56a4e7fd65349d0f89a --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyEvaluationSummaryDTO.java @@ -0,0 +1,64 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.UUID; + +import com.dalab.policyengine.model.PolicyEvaluationStatus; + +public class PolicyEvaluationSummaryDTO { + private UUID id; + private UUID policyId; + private String policyName; + private String targetAssetId; + private PolicyEvaluationStatus status; + private Instant evaluatedAt; + + // Getters and Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getPolicyId() { + return policyId; + } + + public void setPolicyId(UUID policyId) { + this.policyId = policyId; + } + + public String getPolicyName() { + return policyName; + } + + public void setPolicyName(String policyName) { + this.policyName = policyName; + } + + public String getTargetAssetId() { + return targetAssetId; + } + + public void setTargetAssetId(String targetAssetId) { + this.targetAssetId = targetAssetId; + } + + public PolicyEvaluationStatus getStatus() { + return status; + } + + public void setStatus(PolicyEvaluationStatus status) { + this.status = status; + } + + public Instant getEvaluatedAt() { + return evaluatedAt; + } + + public void setEvaluatedAt(Instant evaluatedAt) { + this.evaluatedAt = evaluatedAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyImpactRequestDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyImpactRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..405ba7f50367da5a547ecb5d017035f33cd3ab61 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyImpactRequestDTO.java @@ -0,0 +1,211 @@ +package com.dalab.policyengine.dto; + +import java.util.List; +import java.util.Map; + +import jakarta.validation.constraints.NotBlank; + +/** + * DTO for requesting policy impact analysis preview. + * This allows users to see what assets would be affected by policy changes before implementation. + */ +public class PolicyImpactRequestDTO { + + /** + * The policy rules content to analyze for impact (JSON format). + * Required field containing the policy rules that need impact analysis. + */ + @NotBlank(message = "Policy rules content is required") + private String rulesContent; + + /** + * Type of analysis to perform: FULL, QUICK, or TARGETED. + * - FULL: Comprehensive analysis across all assets + * - QUICK: Fast analysis with sampling + * - TARGETED: Analysis for specific asset types/groups + */ + @NotBlank(message = "Analysis type is required") + private String analysisType; + + /** + * Optional scope limitations for targeted analysis. + * Can include asset types, data sources, or specific asset IDs. + */ + private PolicyImpactScopeDTO scope; + + /** + * Include estimated performance impact in the analysis. + * When true, includes estimates for processing time and resource usage. + */ + private Boolean includePerformanceEstimate = false; + + /** + * Include cost impact analysis in the preview. + * When true, estimates potential costs of policy enforcement. + */ + private Boolean includeCostImpact = false; + + /** + * Include compliance impact analysis. + * When true, shows how this policy affects compliance with other policies. + */ + private Boolean includeComplianceImpact = false; + + /** + * Additional context parameters for policy evaluation. + * Flexible map for custom analysis parameters. + */ + private Map contextParameters; + + // Constructors + public PolicyImpactRequestDTO() {} + + public PolicyImpactRequestDTO(String rulesContent, String analysisType) { + this.rulesContent = rulesContent; + this.analysisType = analysisType; + } + + // Getters and Setters + public String getRulesContent() { + return rulesContent; + } + + public void setRulesContent(String rulesContent) { + this.rulesContent = rulesContent; + } + + public String getAnalysisType() { + return analysisType; + } + + public void setAnalysisType(String analysisType) { + this.analysisType = analysisType; + } + + public PolicyImpactScopeDTO getScope() { + return scope; + } + + public void setScope(PolicyImpactScopeDTO scope) { + this.scope = scope; + } + + public Boolean getIncludePerformanceEstimate() { + return includePerformanceEstimate; + } + + public void setIncludePerformanceEstimate(Boolean includePerformanceEstimate) { + this.includePerformanceEstimate = includePerformanceEstimate; + } + + public Boolean getIncludeCostImpact() { + return includeCostImpact; + } + + public void setIncludeCostImpact(Boolean includeCostImpact) { + this.includeCostImpact = includeCostImpact; + } + + public Boolean getIncludeComplianceImpact() { + return includeComplianceImpact; + } + + public void setIncludeComplianceImpact(Boolean includeComplianceImpact) { + this.includeComplianceImpact = includeComplianceImpact; + } + + public Map getContextParameters() { + return contextParameters; + } + + public void setContextParameters(Map contextParameters) { + this.contextParameters = contextParameters; + } + + /** + * Nested DTO for defining analysis scope limitations. + */ + public static class PolicyImpactScopeDTO { + /** + * Specific asset types to include in analysis (e.g., "database", "file", "api"). + */ + private List assetTypes; + + /** + * Specific data sources to include (e.g., connection IDs or source names). + */ + private List dataSources; + + /** + * Specific asset IDs to analyze (for targeted analysis). + */ + private List assetIds; + + /** + * Include only assets with specific labels. + */ + private List requiredLabels; + + /** + * Exclude assets with specific labels. + */ + private List excludedLabels; + + /** + * Maximum number of assets to analyze (for performance control). + */ + private Integer maxAssets; + + // Constructors + public PolicyImpactScopeDTO() {} + + // Getters and Setters + public List getAssetTypes() { + return assetTypes; + } + + public void setAssetTypes(List assetTypes) { + this.assetTypes = assetTypes; + } + + public List getDataSources() { + return dataSources; + } + + public void setDataSources(List dataSources) { + this.dataSources = dataSources; + } + + public List getAssetIds() { + return assetIds; + } + + public void setAssetIds(List assetIds) { + this.assetIds = assetIds; + } + + public List getRequiredLabels() { + return requiredLabels; + } + + public void setRequiredLabels(List requiredLabels) { + this.requiredLabels = requiredLabels; + } + + public List getExcludedLabels() { + return excludedLabels; + } + + public void setExcludedLabels(List excludedLabels) { + this.excludedLabels = excludedLabels; + } + + public Integer getMaxAssets() { + return maxAssets; + } + + public void setMaxAssets(Integer maxAssets) { + this.maxAssets = maxAssets; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyImpactResponseDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyImpactResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..1b07eb14e66fc0d1f6293291d079bd351870bde1 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyImpactResponseDTO.java @@ -0,0 +1,376 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * Comprehensive DTO for policy impact analysis response. + * Contains detailed analysis of what assets would be affected by policy changes. + */ +public class PolicyImpactResponseDTO { + + /** + * Unique identifier for this impact analysis. + */ + private String analysisId; + + /** + * Timestamp when the analysis was performed. + */ + private Instant analyzedAt; + + /** + * Type of analysis performed: FULL, QUICK, or TARGETED. + */ + private String analysisType; + + /** + * Overall impact summary with key metrics. + */ + private ImpactSummaryDTO summary; + + /** + * Detailed breakdown of affected assets by category. + */ + private List affectedAssets; + + /** + * Performance impact estimates (if requested). + */ + private PerformanceImpactDTO performanceImpact; + + /** + * Cost impact analysis (if requested). + */ + private CostImpactDTO costImpact; + + /** + * Compliance impact analysis (if requested). + */ + private ComplianceImpactDTO complianceImpact; + + /** + * Risk assessment for implementing this policy. + */ + private RiskAssessmentDTO riskAssessment; + + /** + * Recommendations for policy implementation. + */ + private List recommendations; + + /** + * Analysis execution metadata. + */ + private AnalysisMetadataDTO metadata; + + // Constructors + public PolicyImpactResponseDTO() {} + + public PolicyImpactResponseDTO(String analysisId, String analysisType) { + this.analysisId = analysisId; + this.analysisType = analysisType; + this.analyzedAt = Instant.now(); + } + + // Getters and Setters + public String getAnalysisId() { + return analysisId; + } + + public void setAnalysisId(String analysisId) { + this.analysisId = analysisId; + } + + public Instant getAnalyzedAt() { + return analyzedAt; + } + + public void setAnalyzedAt(Instant analyzedAt) { + this.analyzedAt = analyzedAt; + } + + public String getAnalysisType() { + return analysisType; + } + + public void setAnalysisType(String analysisType) { + this.analysisType = analysisType; + } + + public ImpactSummaryDTO getSummary() { + return summary; + } + + public void setSummary(ImpactSummaryDTO summary) { + this.summary = summary; + } + + public List getAffectedAssets() { + return affectedAssets; + } + + public void setAffectedAssets(List affectedAssets) { + this.affectedAssets = affectedAssets; + } + + public PerformanceImpactDTO getPerformanceImpact() { + return performanceImpact; + } + + public void setPerformanceImpact(PerformanceImpactDTO performanceImpact) { + this.performanceImpact = performanceImpact; + } + + public CostImpactDTO getCostImpact() { + return costImpact; + } + + public void setCostImpact(CostImpactDTO costImpact) { + this.costImpact = costImpact; + } + + public ComplianceImpactDTO getComplianceImpact() { + return complianceImpact; + } + + public void setComplianceImpact(ComplianceImpactDTO complianceImpact) { + this.complianceImpact = complianceImpact; + } + + public RiskAssessmentDTO getRiskAssessment() { + return riskAssessment; + } + + public void setRiskAssessment(RiskAssessmentDTO riskAssessment) { + this.riskAssessment = riskAssessment; + } + + public List getRecommendations() { + return recommendations; + } + + public void setRecommendations(List recommendations) { + this.recommendations = recommendations; + } + + public AnalysisMetadataDTO getMetadata() { + return metadata; + } + + public void setMetadata(AnalysisMetadataDTO metadata) { + this.metadata = metadata; + } + + /** + * Overall impact summary with key metrics. + */ + public static class ImpactSummaryDTO { + private Integer totalAssetsAnalyzed; + private Integer totalAssetsAffected; + private Integer highImpactAssets; + private Integer mediumImpactAssets; + private Integer lowImpactAssets; + private String overallRiskLevel; // LOW, MEDIUM, HIGH, CRITICAL + private Double impactPercentage; + + // Constructors, getters, and setters + public ImpactSummaryDTO() {} + + public Integer getTotalAssetsAnalyzed() { return totalAssetsAnalyzed; } + public void setTotalAssetsAnalyzed(Integer totalAssetsAnalyzed) { this.totalAssetsAnalyzed = totalAssetsAnalyzed; } + + public Integer getTotalAssetsAffected() { return totalAssetsAffected; } + public void setTotalAssetsAffected(Integer totalAssetsAffected) { this.totalAssetsAffected = totalAssetsAffected; } + + public Integer getHighImpactAssets() { return highImpactAssets; } + public void setHighImpactAssets(Integer highImpactAssets) { this.highImpactAssets = highImpactAssets; } + + public Integer getMediumImpactAssets() { return mediumImpactAssets; } + public void setMediumImpactAssets(Integer mediumImpactAssets) { this.mediumImpactAssets = mediumImpactAssets; } + + public Integer getLowImpactAssets() { return lowImpactAssets; } + public void setLowImpactAssets(Integer lowImpactAssets) { this.lowImpactAssets = lowImpactAssets; } + + public String getOverallRiskLevel() { return overallRiskLevel; } + public void setOverallRiskLevel(String overallRiskLevel) { this.overallRiskLevel = overallRiskLevel; } + + public Double getImpactPercentage() { return impactPercentage; } + public void setImpactPercentage(Double impactPercentage) { this.impactPercentage = impactPercentage; } + } + + /** + * Individual asset impact details. + */ + public static class AssetImpactDTO { + private String assetId; + private String assetName; + private String assetType; + private String impactLevel; // HIGH, MEDIUM, LOW + private List affectedAttributes; + private List appliedActions; + private String riskAssessment; + private Map impactDetails; + + // Constructors, getters, and setters + public AssetImpactDTO() {} + + public String getAssetId() { return assetId; } + public void setAssetId(String assetId) { this.assetId = assetId; } + + public String getAssetName() { return assetName; } + public void setAssetName(String assetName) { this.assetName = assetName; } + + public String getAssetType() { return assetType; } + public void setAssetType(String assetType) { this.assetType = assetType; } + + public String getImpactLevel() { return impactLevel; } + public void setImpactLevel(String impactLevel) { this.impactLevel = impactLevel; } + + public List getAffectedAttributes() { return affectedAttributes; } + public void setAffectedAttributes(List affectedAttributes) { this.affectedAttributes = affectedAttributes; } + + public List getAppliedActions() { return appliedActions; } + public void setAppliedActions(List appliedActions) { this.appliedActions = appliedActions; } + + public String getRiskAssessment() { return riskAssessment; } + public void setRiskAssessment(String riskAssessment) { this.riskAssessment = riskAssessment; } + + public Map getImpactDetails() { return impactDetails; } + public void setImpactDetails(Map impactDetails) { this.impactDetails = impactDetails; } + } + + /** + * Performance impact estimates. + */ + public static class PerformanceImpactDTO { + private Long estimatedProcessingTimeMs; + private Double cpuUtilizationIncrease; + private Double memoryUtilizationIncrease; + private Integer estimatedApiCalls; + private String performanceRiskLevel; + + // Constructors, getters, and setters + public PerformanceImpactDTO() {} + + public Long getEstimatedProcessingTimeMs() { return estimatedProcessingTimeMs; } + public void setEstimatedProcessingTimeMs(Long estimatedProcessingTimeMs) { this.estimatedProcessingTimeMs = estimatedProcessingTimeMs; } + + public Double getCpuUtilizationIncrease() { return cpuUtilizationIncrease; } + public void setCpuUtilizationIncrease(Double cpuUtilizationIncrease) { this.cpuUtilizationIncrease = cpuUtilizationIncrease; } + + public Double getMemoryUtilizationIncrease() { return memoryUtilizationIncrease; } + public void setMemoryUtilizationIncrease(Double memoryUtilizationIncrease) { this.memoryUtilizationIncrease = memoryUtilizationIncrease; } + + public Integer getEstimatedApiCalls() { return estimatedApiCalls; } + public void setEstimatedApiCalls(Integer estimatedApiCalls) { this.estimatedApiCalls = estimatedApiCalls; } + + public String getPerformanceRiskLevel() { return performanceRiskLevel; } + public void setPerformanceRiskLevel(String performanceRiskLevel) { this.performanceRiskLevel = performanceRiskLevel; } + } + + /** + * Cost impact analysis. + */ + public static class CostImpactDTO { + private Double estimatedMonthlyCost; + private Double estimatedImplementationCost; + private Double potentialSavings; + private String costRiskLevel; + private Map costBreakdown; + + // Constructors, getters, and setters + public CostImpactDTO() {} + + public Double getEstimatedMonthlyCost() { return estimatedMonthlyCost; } + public void setEstimatedMonthlyCost(Double estimatedMonthlyCost) { this.estimatedMonthlyCost = estimatedMonthlyCost; } + + public Double getEstimatedImplementationCost() { return estimatedImplementationCost; } + public void setEstimatedImplementationCost(Double estimatedImplementationCost) { this.estimatedImplementationCost = estimatedImplementationCost; } + + public Double getPotentialSavings() { return potentialSavings; } + public void setPotentialSavings(Double potentialSavings) { this.potentialSavings = potentialSavings; } + + public String getCostRiskLevel() { return costRiskLevel; } + public void setCostRiskLevel(String costRiskLevel) { this.costRiskLevel = costRiskLevel; } + + public Map getCostBreakdown() { return costBreakdown; } + public void setCostBreakdown(Map costBreakdown) { this.costBreakdown = costBreakdown; } + } + + /** + * Compliance impact analysis. + */ + public static class ComplianceImpactDTO { + private Integer conflictingPolicies; + private List complianceFrameworksAffected; + private String complianceRiskLevel; + private List potentialViolations; + + // Constructors, getters, and setters + public ComplianceImpactDTO() {} + + public Integer getConflictingPolicies() { return conflictingPolicies; } + public void setConflictingPolicies(Integer conflictingPolicies) { this.conflictingPolicies = conflictingPolicies; } + + public List getComplianceFrameworksAffected() { return complianceFrameworksAffected; } + public void setComplianceFrameworksAffected(List complianceFrameworksAffected) { this.complianceFrameworksAffected = complianceFrameworksAffected; } + + public String getComplianceRiskLevel() { return complianceRiskLevel; } + public void setComplianceRiskLevel(String complianceRiskLevel) { this.complianceRiskLevel = complianceRiskLevel; } + + public List getPotentialViolations() { return potentialViolations; } + public void setPotentialViolations(List potentialViolations) { this.potentialViolations = potentialViolations; } + } + + /** + * Risk assessment for policy implementation. + */ + public static class RiskAssessmentDTO { + private String overallRiskLevel; + private List identifiedRisks; + private List mitigationStrategies; + private Double riskScore; + + // Constructors, getters, and setters + public RiskAssessmentDTO() {} + + public String getOverallRiskLevel() { return overallRiskLevel; } + public void setOverallRiskLevel(String overallRiskLevel) { this.overallRiskLevel = overallRiskLevel; } + + public List getIdentifiedRisks() { return identifiedRisks; } + public void setIdentifiedRisks(List identifiedRisks) { this.identifiedRisks = identifiedRisks; } + + public List getMitigationStrategies() { return mitigationStrategies; } + public void setMitigationStrategies(List mitigationStrategies) { this.mitigationStrategies = mitigationStrategies; } + + public Double getRiskScore() { return riskScore; } + public void setRiskScore(Double riskScore) { this.riskScore = riskScore; } + } + + /** + * Analysis execution metadata. + */ + public static class AnalysisMetadataDTO { + private Long executionTimeMs; + private String analysisVersion; + private Boolean usesCachedData; + private Instant dataFreshnessTimestamp; + + // Constructors, getters, and setters + public AnalysisMetadataDTO() {} + + public Long getExecutionTimeMs() { return executionTimeMs; } + public void setExecutionTimeMs(Long executionTimeMs) { this.executionTimeMs = executionTimeMs; } + + public String getAnalysisVersion() { return analysisVersion; } + public void setAnalysisVersion(String analysisVersion) { this.analysisVersion = analysisVersion; } + + public Boolean getUsesCachedData() { return usesCachedData; } + public void setUsesCachedData(Boolean usesCachedData) { this.usesCachedData = usesCachedData; } + + public Instant getDataFreshnessTimestamp() { return dataFreshnessTimestamp; } + public void setDataFreshnessTimestamp(Instant dataFreshnessTimestamp) { this.dataFreshnessTimestamp = dataFreshnessTimestamp; } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyInputDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyInputDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..e4c3ba77f10b8f123db0447c2a50a3e3447c87f5 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyInputDTO.java @@ -0,0 +1,80 @@ +package com.dalab.policyengine.dto; + +import java.util.List; +import java.util.Map; + +import com.dalab.policyengine.model.PolicyStatus; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +public class PolicyInputDTO { + + @NotBlank + @Size(max = 255) + private String name; + + @Size(max = 1000) + private String description; + + private PolicyStatus status = PolicyStatus.DISABLED; + + private String conditionLogic; // e.g., "rule1 && (rule2 || rule3)" + + @NotEmpty + @Valid + private List rules; + + private Map actions; // Policy-level actions + + // Getters and Setters + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public PolicyStatus getStatus() { + return status; + } + + public void setStatus(PolicyStatus status) { + this.status = status; + } + + public String getConditionLogic() { + return conditionLogic; + } + + public void setConditionLogic(String conditionLogic) { + this.conditionLogic = conditionLogic; + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules; + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyOutputDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyOutputDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..166c1c86bafbe44d54eaa6b71f36d55231461be7 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyOutputDTO.java @@ -0,0 +1,111 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.dalab.policyengine.model.PolicyStatus; + +public class PolicyOutputDTO { + private UUID id; + private String name; + private String description; + private PolicyStatus status; + private String conditionLogic; + private List rules; + private Map actions; + private Instant createdAt; + private Instant updatedAt; + private UUID createdByUserId; + private UUID updatedByUserId; + + // Getters and Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public PolicyStatus getStatus() { + return status; + } + + public void setStatus(PolicyStatus status) { + this.status = status; + } + + public String getConditionLogic() { + return conditionLogic; + } + + public void setConditionLogic(String conditionLogic) { + this.conditionLogic = conditionLogic; + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules; + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/dto/PolicyRuleDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicyRuleDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..0b43a59c8b3f02a9e92c4c89815395b7c0b90514 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicyRuleDTO.java @@ -0,0 +1,72 @@ +package com.dalab.policyengine.dto; + +import java.util.Map; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class PolicyRuleDTO { + private String id; // UUID as String, can be null for new rules in create/update + + @NotBlank + @Size(max = 255) + private String name; + + @Size(max = 1000) + private String description; + + @NotBlank + private String condition; // MVEL expression + + private int priority = 1; + private Map actions; // Optional rule-specific actions + + // Getters and Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } +} diff --git a/src/main/java/com/dalab/policyengine/dto/PolicySummaryDTO.java b/src/main/java/com/dalab/policyengine/dto/PolicySummaryDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..656f73fc0e877fe103362f7796eca78428b0e8d2 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/dto/PolicySummaryDTO.java @@ -0,0 +1,73 @@ +package com.dalab.policyengine.dto; + +import java.time.Instant; +import java.util.UUID; + +import com.dalab.policyengine.model.PolicyStatus; + +public class PolicySummaryDTO { + private UUID id; + private String name; + private String description; + private PolicyStatus status; + private int ruleCount; + private Instant createdAt; + private Instant updatedAt; + + // Getters and Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public PolicyStatus getStatus() { + return status; + } + + public void setStatus(PolicyStatus status) { + this.status = status; + } + + public int getRuleCount() { + return ruleCount; + } + + public void setRuleCount(int ruleCount) { + this.ruleCount = ruleCount; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/event/PolicyActionEvent.java b/src/main/java/com/dalab/policyengine/event/PolicyActionEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..ac9227561f6e8f39c5c9e4ebacb05ff1fd7e4c22 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/event/PolicyActionEvent.java @@ -0,0 +1,102 @@ +package com.dalab.policyengine.event; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +// This DTO represents an action triggered by a policy evaluation, +// to be published to a Kafka topic. +public class PolicyActionEvent { + private UUID eventId; + private String policyId; + private String policyName; + private String targetAssetId; + private String actionType; // e.g., "ADD_LABEL", "NOTIFY_EMAIL", "ARCHIVE_ASSET" + private Map actionParameters; // Parameters for the action, e.g., {"labelName": "PII", "confidence": 0.9} or {"to": "user@example.com", "subject": "Alert"} + private Instant timestamp; + private UUID triggeredByEvaluationId; // Link back to the PolicyEvaluation record + + public PolicyActionEvent() { + this.eventId = UUID.randomUUID(); + this.timestamp = Instant.now(); + } + + // Getters and Setters + public UUID getEventId() { + return eventId; + } + + public void setEventId(UUID eventId) { + this.eventId = eventId; + } + + public String getPolicyId() { + return policyId; + } + + public void setPolicyId(String policyId) { + this.policyId = policyId; + } + + public String getPolicyName() { + return policyName; + } + + public void setPolicyName(String policyName) { + this.policyName = policyName; + } + + public String getTargetAssetId() { + return targetAssetId; + } + + public void setTargetAssetId(String targetAssetId) { + this.targetAssetId = targetAssetId; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public Map getActionParameters() { + return actionParameters; + } + + public void setActionParameters(Map actionParameters) { + this.actionParameters = actionParameters; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + public UUID getTriggeredByEvaluationId() { + return triggeredByEvaluationId; + } + + public void setTriggeredByEvaluationId(UUID triggeredByEvaluationId) { + this.triggeredByEvaluationId = triggeredByEvaluationId; + } + + @Override + public String toString() { + return "PolicyActionEvent{" + + "eventId=" + eventId + + ", policyId='" + policyId + '\'' + + ", policyName='" + policyName + '\'' + + ", targetAssetId='" + targetAssetId + '\'' + + ", actionType='" + actionType + '\'' + + ", actionParameters=" + actionParameters + + ", timestamp=" + timestamp + + ", triggeredByEvaluationId=" + triggeredByEvaluationId + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/kafka/consumer/AssetChangeConsumer.java b/src/main/java/com/dalab/policyengine/kafka/consumer/AssetChangeConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..f947fabffe0b955e07119ed044692bed666512a8 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/kafka/consumer/AssetChangeConsumer.java @@ -0,0 +1,50 @@ +package com.dalab.policyengine.kafka.consumer; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +// Import the common AssetChangeEvent from da-protos +import com.dalab.common.event.AssetChangeEvent; +import com.dalab.policyengine.service.IPolicyEvaluationService; + +@Component +public class AssetChangeConsumer { + + private static final Logger log = LoggerFactory.getLogger(AssetChangeConsumer.class); + // System UUID for actions triggered by Kafka consumer events + private static final UUID KAFKA_CONSUMER_USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000001"); + + private final IPolicyEvaluationService policyEvaluationService; + + @Autowired + public AssetChangeConsumer(IPolicyEvaluationService policyEvaluationService) { + this.policyEvaluationService = policyEvaluationService; + } + + // Update KafkaListener to consume the common AssetChangeEvent + @KafkaListener(topics = "${app.kafka.topic.asset-change-event:asset-change-events}", groupId = "${spring.kafka.consumer.group-id}") + public void handleAssetChangeEvent(@Payload AssetChangeEvent event) { + log.info("Received AssetChangeEvent: AssetId={}, EventType={}", event.getAssetId(), event.getEventType()); + + try { + if (event == null || event.getAssetId() == null || !StringUtils.hasText(event.getAssetId())) { + log.error("AssetChangeEvent is null or missing assetId. Skipping."); + return; + } + // The PolicyEvaluationService will now fetch active policies and iterate. + policyEvaluationService.evaluatePolicyForAssetInternal(event, KAFKA_CONSUMER_USER_ID); + + } catch (Exception e) { + log.error("Failed to process AssetChangeEvent for assetId {}: {}. Error: {}", + event != null && event.getAssetId() != null ? event.getAssetId() : "unknown", + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/kafka/consumer/AssetChangeEventListener.java b/src/main/java/com/dalab/policyengine/kafka/consumer/AssetChangeEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..e7e25aebd0953fa1dd176e8fe03ce961b0a9ef66 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/kafka/consumer/AssetChangeEventListener.java @@ -0,0 +1,61 @@ +package com.dalab.policyengine.kafka.consumer; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Component; + +import com.dalab.common.event.AssetChangeEvent; +import com.dalab.policyengine.service.IPolicyEvaluationService; + +@Component +public class AssetChangeEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(AssetChangeEventListener.class); + + private final IPolicyEvaluationService policyEvaluationService; + + public AssetChangeEventListener(IPolicyEvaluationService policyEvaluationService) { + this.policyEvaluationService = policyEvaluationService; + } + + @KafkaListener( + topics = "#{'${app.kafka.topic.asset-change-event}'}", + groupId = "#{'${spring.kafka.consumer.group-id}'}" + // containerFactory = "assetChangeKafkaListenerContainerFactory" // If using custom factory + ) + public void handleAssetChangeEvent( + @Payload AssetChangeEvent event, + @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, + @Header(KafkaHeaders.RECEIVED_PARTITION) int partition, + @Header(KafkaHeaders.OFFSET) long offset + ) { + LOGGER.info( + "Received AssetChangeEvent on topic: {}, partition: {}, offset: {}: Asset ID: {}, EventType: {}", + topic, partition, offset, event.getAssetId(), event.getEventType() + ); + + try { + // Trigger policy evaluation based on the asset change + UUID initiatorUuid = null; + if (event.getUserId() != null && !event.getUserId().isEmpty()) { + try { + initiatorUuid = UUID.fromString(event.getUserId()); + } catch (IllegalArgumentException e) { + LOGGER.warn("Could not parse userId '{}' from AssetChangeEvent to UUID. Proceeding with null initiator.", event.getUserId(), e); + } + } + policyEvaluationService.evaluatePolicyForAssetInternal(event, initiatorUuid); + LOGGER.debug("Successfully processed AssetChangeEvent for assetId: {}", event.getAssetId()); + } catch (Exception e) { + // TODO: Implement proper error handling and dead-letter queue (DLQ) strategy + LOGGER.error("Error processing AssetChangeEvent for assetId: {}. Error: {}", event.getAssetId(), e.getMessage(), e); + // Depending on the error, you might rethrow to trigger Kafka's error handlers, or send to DLQ + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/kafka/producer/PolicyActionKafkaProducer.java b/src/main/java/com/dalab/policyengine/kafka/producer/PolicyActionKafkaProducer.java new file mode 100644 index 0000000000000000000000000000000000000000..6787402bf33c6d02aef2df344e87f4dcfdd0a427 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/kafka/producer/PolicyActionKafkaProducer.java @@ -0,0 +1,54 @@ +package com.dalab.policyengine.kafka.producer; + +import java.util.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; + +import com.dalab.policyengine.event.PolicyActionEvent; + +@Component +public class PolicyActionKafkaProducer { + + private static final Logger LOGGER = LoggerFactory.getLogger(PolicyActionKafkaProducer.class); + + private final KafkaTemplate kafkaTemplate; + + @Value("${app.kafka.topic.policy-action-event}") + private String topicName; + + public PolicyActionKafkaProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + public void sendPolicyActionEvent(PolicyActionEvent event) { + LOGGER.info("Sending PolicyActionEvent with eventId: {} to topic: {}", event.getEventId(), topicName); + + String eventKey = event.getTargetAssetId(); + if (eventKey == null || eventKey.isEmpty()) { + eventKey = event.getPolicyId(); + } + final String finalEventKey = eventKey; + + CompletableFuture> future = kafkaTemplate.send(topicName, finalEventKey, event); + + future.whenComplete((result, ex) -> { + if (ex == null) { + LOGGER.info( + "Successfully sent PolicyActionEvent [eventId: {}, key: {}] with offset: {}", + event.getEventId(), finalEventKey, result.getRecordMetadata().offset() + ); + } else { + LOGGER.error( + "Failed to send PolicyActionEvent [eventId: {}, key: {}]: {}", + event.getEventId(), finalEventKey, ex.getMessage(), ex + ); + // TODO: Handle send failure (e.g., retry, DLQ, persistent store for later retry) + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/kafka/producer/PolicyActionProducer.java b/src/main/java/com/dalab/policyengine/kafka/producer/PolicyActionProducer.java new file mode 100644 index 0000000000000000000000000000000000000000..7bf603abf125cafc7d3f8794484328d90773e05f --- /dev/null +++ b/src/main/java/com/dalab/policyengine/kafka/producer/PolicyActionProducer.java @@ -0,0 +1,42 @@ +package com.dalab.policyengine.kafka.producer; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@Service +public class PolicyActionProducer { + + private static final Logger log = LoggerFactory.getLogger(PolicyActionProducer.class); + + @Value("${app.kafka.topic.policy-action-event:policy-action-events}") + private String policyActionEventTopic; + + // Use generic KafkaTemplate since we're sending Map-based events for now + private final KafkaTemplate kafkaTemplate; + + @Autowired + public PolicyActionProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + // Method to send policy action events as Map objects for now + public void sendPolicyActionEvent(Map event) { + try { + String assetId = (String) event.get("assetId"); + String actionType = (String) event.get("actionType"); + log.info("Sending PolicyActionEvent to topic '{}': AssetId={}, ActionType={}", + policyActionEventTopic, assetId, actionType); + // Use assetId as key for partitioning + kafkaTemplate.send(policyActionEventTopic, assetId, event); + } catch (Exception e) { + String assetId = event != null ? (String) event.get("assetId") : "unknown"; + log.error("Error sending PolicyActionEvent to Kafka for assetId {}: {}", assetId, e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/mapper/PolicyDraftMapper.java b/src/main/java/com/dalab/policyengine/mapper/PolicyDraftMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..17a9b4b022df826941d0be0568524c303927bb61 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/mapper/PolicyDraftMapper.java @@ -0,0 +1,372 @@ +package com.dalab.policyengine.mapper; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.dalab.policyengine.dto.PolicyDraftInputDTO; +import com.dalab.policyengine.dto.PolicyDraftOutputDTO; +import com.dalab.policyengine.model.PolicyDraft; +import com.dalab.policyengine.model.PolicyDraftStatus; + +/** + * Mapper for converting between PolicyDraft entities and DTOs. + * Handles workflow status calculation and available actions based on current status and user permissions. + */ +@Component +public class PolicyDraftMapper { + + /** + * Convert PolicyDraft entity to output DTO with workflow information. + */ + public PolicyDraftOutputDTO toOutputDTO(PolicyDraft draft) { + if (draft == null) { + return null; + } + + PolicyDraftOutputDTO dto = new PolicyDraftOutputDTO(); + + // Basic information + dto.setId(draft.getId()); + dto.setName(draft.getName()); + dto.setDescription(draft.getDescription()); + dto.setStatus(draft.getStatus().name()); + dto.setVersion(draft.getVersion()); + dto.setBasePolicyId(draft.getBasePolicyId()); + dto.setConditionLogic(draft.getConditionLogic()); + dto.setRulesDefinition(draft.getRulesDefinition()); + dto.setActions(draft.getActions()); + dto.setChangeSummary(draft.getChangeSummary()); + dto.setJustification(draft.getJustification()); + dto.setExpectedImpact(draft.getExpectedImpact()); + dto.setTargetImplementationDate(draft.getTargetImplementationDate()); + dto.setPriority(draft.getPriority()); + dto.setCategory(draft.getCategory()); + dto.setTags(draft.getTags()); + dto.setStakeholders(draft.getStakeholders()); + dto.setApprovalMetadata(draft.getApprovalMetadata()); + dto.setReviewComments(draft.getReviewComments()); + + // Audit trail + dto.setCreatedAt(draft.getCreatedAt()); + dto.setUpdatedAt(draft.getUpdatedAt()); + dto.setCreatedByUserId(draft.getCreatedByUserId()); + dto.setUpdatedByUserId(draft.getUpdatedByUserId()); + dto.setSubmittedAt(draft.getSubmittedAt()); + dto.setSubmittedByUserId(draft.getSubmittedByUserId()); + dto.setApprovedAt(draft.getApprovedAt()); + dto.setApprovedByUserId(draft.getApprovedByUserId()); + dto.setRejectedAt(draft.getRejectedAt()); + dto.setRejectedByUserId(draft.getRejectedByUserId()); + dto.setPublishedAt(draft.getPublishedAt()); + dto.setPublishedByUserId(draft.getPublishedByUserId()); + + // Enhanced workflow information + dto.setWorkflowStatus(createWorkflowStatus(draft)); + dto.setAvailableActions(createAvailableActions(draft)); + + return dto; + } + + /** + * Convert input DTO to PolicyDraft entity. + */ + public PolicyDraft toEntity(PolicyDraftInputDTO inputDTO) { + if (inputDTO == null) { + return null; + } + + PolicyDraft draft = new PolicyDraft(); + + draft.setName(inputDTO.getName()); + draft.setDescription(inputDTO.getDescription()); + draft.setBasePolicyId(inputDTO.getBasePolicyId()); + draft.setConditionLogic(inputDTO.getConditionLogic()); + draft.setRulesDefinition(inputDTO.getRulesDefinition()); + draft.setActions(inputDTO.getActions()); + draft.setChangeSummary(inputDTO.getChangeSummary()); + draft.setJustification(inputDTO.getJustification()); + draft.setExpectedImpact(inputDTO.getExpectedImpact()); + draft.setTargetImplementationDate(inputDTO.getTargetImplementationDate()); + draft.setPriority(inputDTO.getPriority()); + draft.setCategory(inputDTO.getCategory()); + + if (inputDTO.getTags() != null) { + draft.setTags(new ArrayList<>(inputDTO.getTags())); + } + if (inputDTO.getStakeholders() != null) { + draft.setStakeholders(new ArrayList<>(inputDTO.getStakeholders())); + } + + return draft; + } + + /** + * Update entity from input DTO (for updates). + */ + public void updateEntity(PolicyDraft draft, PolicyDraftInputDTO inputDTO) { + if (draft == null || inputDTO == null) { + return; + } + + draft.setName(inputDTO.getName()); + draft.setDescription(inputDTO.getDescription()); + draft.setBasePolicyId(inputDTO.getBasePolicyId()); + draft.setConditionLogic(inputDTO.getConditionLogic()); + draft.setRulesDefinition(inputDTO.getRulesDefinition()); + draft.setActions(inputDTO.getActions()); + draft.setChangeSummary(inputDTO.getChangeSummary()); + draft.setJustification(inputDTO.getJustification()); + draft.setExpectedImpact(inputDTO.getExpectedImpact()); + draft.setTargetImplementationDate(inputDTO.getTargetImplementationDate()); + draft.setPriority(inputDTO.getPriority()); + draft.setCategory(inputDTO.getCategory()); + + if (inputDTO.getTags() != null) { + draft.getTags().clear(); + draft.getTags().addAll(inputDTO.getTags()); + } + if (inputDTO.getStakeholders() != null) { + draft.getStakeholders().clear(); + draft.getStakeholders().addAll(inputDTO.getStakeholders()); + } + } + + /** + * Create workflow status information for the draft. + */ + private PolicyDraftOutputDTO.WorkflowStatusDTO createWorkflowStatus(PolicyDraft draft) { + PolicyDraftOutputDTO.WorkflowStatusDTO status = new PolicyDraftOutputDTO.WorkflowStatusDTO(); + + PolicyDraftStatus currentStatus = draft.getStatus(); + status.setCurrentStage(currentStatus.name()); + status.setStatusDescription(getStatusDescription(currentStatus)); + status.setNextPossibleStates(getNextPossibleStates(currentStatus)); + status.setStageColor(getStageColor(currentStatus)); + + // Calculate days in current status + Instant statusChangeTime = getLastStatusChangeTime(draft, currentStatus); + if (statusChangeTime != null) { + long days = ChronoUnit.DAYS.between(statusChangeTime, Instant.now()); + status.setDaysInCurrentStatus((int) days); + } + + // Set permission flags (simplified - would integrate with security context in real implementation) + status.setCanEdit(canEdit(currentStatus)); + status.setCanSubmit(canSubmit(currentStatus)); + status.setCanApprove(canApprove(currentStatus)); + status.setCanReject(canReject(currentStatus)); + status.setCanPublish(canPublish(currentStatus)); + + return status; + } + + /** + * Create available workflow actions for the draft. + */ + private List createAvailableActions(PolicyDraft draft) { + List actions = new ArrayList<>(); + PolicyDraftStatus status = draft.getStatus(); + + switch (status) { + case CREATED: + case REQUIRES_CHANGES: + actions.add(new PolicyDraftOutputDTO.WorkflowActionDTO("SUBMIT", "Submit for Review")); + actions.add(new PolicyDraftOutputDTO.WorkflowActionDTO("ARCHIVE", "Archive Draft")); + break; + case SUBMITTED: + case UNDER_REVIEW: + actions.add(new PolicyDraftOutputDTO.WorkflowActionDTO("APPROVE", "Approve Draft")); + actions.add(new PolicyDraftOutputDTO.WorkflowActionDTO("REJECT", "Reject Draft")); + actions.add(new PolicyDraftOutputDTO.WorkflowActionDTO("REQUEST_CHANGES", "Request Changes")); + break; + case APPROVED: + actions.add(new PolicyDraftOutputDTO.WorkflowActionDTO("PUBLISH", "Publish Policy")); + actions.add(new PolicyDraftOutputDTO.WorkflowActionDTO("REJECT", "Reject Draft")); + break; + default: + // No actions available for REJECTED, PUBLISHED, ARCHIVED + break; + } + + // Set action properties + for (PolicyDraftOutputDTO.WorkflowActionDTO action : actions) { + setActionProperties(action); + } + + return actions; + } + + /** + * Get human-readable description for status. + */ + private String getStatusDescription(PolicyDraftStatus status) { + switch (status) { + case CREATED: + return "Draft has been created and is ready for editing"; + case SUBMITTED: + return "Draft has been submitted and is awaiting review"; + case UNDER_REVIEW: + return "Draft is currently being reviewed"; + case REQUIRES_CHANGES: + return "Draft requires changes based on reviewer feedback"; + case APPROVED: + return "Draft has been approved and is ready for publication"; + case REJECTED: + return "Draft has been rejected and will not be published"; + case PUBLISHED: + return "Draft has been published as an active policy"; + case ARCHIVED: + return "Draft has been archived and is no longer active"; + default: + return "Unknown status"; + } + } + + /** + * Get next possible states for workflow transitions. + */ + private List getNextPossibleStates(PolicyDraftStatus status) { + switch (status) { + case CREATED: + case REQUIRES_CHANGES: + return Arrays.asList("SUBMITTED", "ARCHIVED"); + case SUBMITTED: + return Arrays.asList("UNDER_REVIEW", "APPROVED", "REJECTED", "REQUIRES_CHANGES"); + case UNDER_REVIEW: + return Arrays.asList("APPROVED", "REJECTED", "REQUIRES_CHANGES"); + case APPROVED: + return Arrays.asList("PUBLISHED", "REJECTED"); + default: + return new ArrayList<>(); // Terminal states + } + } + + /** + * Get color for UI status indicators. + */ + private String getStageColor(PolicyDraftStatus status) { + switch (status) { + case CREATED: + return "blue"; + case SUBMITTED: + case UNDER_REVIEW: + return "orange"; + case REQUIRES_CHANGES: + return "yellow"; + case APPROVED: + return "green"; + case REJECTED: + return "red"; + case PUBLISHED: + return "purple"; + case ARCHIVED: + return "gray"; + default: + return "gray"; + } + } + + /** + * Get the timestamp of last status change. + */ + private Instant getLastStatusChangeTime(PolicyDraft draft, PolicyDraftStatus currentStatus) { + switch (currentStatus) { + case SUBMITTED: + return draft.getSubmittedAt(); + case APPROVED: + return draft.getApprovedAt(); + case REJECTED: + return draft.getRejectedAt(); + case PUBLISHED: + return draft.getPublishedAt(); + default: + return draft.getUpdatedAt(); + } + } + + /** + * Check if draft can be edited in current status. + */ + private boolean canEdit(PolicyDraftStatus status) { + return status == PolicyDraftStatus.CREATED || status == PolicyDraftStatus.REQUIRES_CHANGES; + } + + /** + * Check if draft can be submitted in current status. + */ + private boolean canSubmit(PolicyDraftStatus status) { + return status == PolicyDraftStatus.CREATED || status == PolicyDraftStatus.REQUIRES_CHANGES; + } + + /** + * Check if draft can be approved in current status. + */ + private boolean canApprove(PolicyDraftStatus status) { + return status == PolicyDraftStatus.SUBMITTED || status == PolicyDraftStatus.UNDER_REVIEW; + } + + /** + * Check if draft can be rejected in current status. + */ + private boolean canReject(PolicyDraftStatus status) { + return status == PolicyDraftStatus.SUBMITTED || + status == PolicyDraftStatus.UNDER_REVIEW || + status == PolicyDraftStatus.APPROVED; + } + + /** + * Check if draft can be published in current status. + */ + private boolean canPublish(PolicyDraftStatus status) { + return status == PolicyDraftStatus.APPROVED; + } + + /** + * Set properties for workflow actions. + */ + private void setActionProperties(PolicyDraftOutputDTO.WorkflowActionDTO action) { + switch (action.getActionType()) { + case "SUBMIT": + action.setActionDescription("Submit the draft for review by approvers"); + action.setRequiresComment(false); + action.setConfirmationMessage("Are you sure you want to submit this draft for review?"); + action.setButtonStyle("primary"); + break; + case "APPROVE": + action.setActionDescription("Approve the draft for publication"); + action.setRequiresComment(false); + action.setConfirmationMessage("Are you sure you want to approve this draft?"); + action.setButtonStyle("success"); + break; + case "REJECT": + action.setActionDescription("Reject the draft and prevent publication"); + action.setRequiresComment(true); + action.setConfirmationMessage("Are you sure you want to reject this draft? This action cannot be undone."); + action.setButtonStyle("danger"); + break; + case "REQUEST_CHANGES": + action.setActionDescription("Request changes from the draft author"); + action.setRequiresComment(true); + action.setConfirmationMessage("Please provide details about the required changes"); + action.setButtonStyle("warning"); + break; + case "PUBLISH": + action.setActionDescription("Publish the draft as an active policy"); + action.setRequiresComment(false); + action.setConfirmationMessage("Are you sure you want to publish this draft as an active policy?"); + action.setButtonStyle("success"); + break; + case "ARCHIVE": + action.setActionDescription("Archive the draft"); + action.setRequiresComment(false); + action.setConfirmationMessage("Are you sure you want to archive this draft?"); + action.setButtonStyle("secondary"); + break; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/mapper/PolicyMapper.java b/src/main/java/com/dalab/policyengine/mapper/PolicyMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..06d5ff8ad2a296913f4b54a11fbbbb8913c26328 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/mapper/PolicyMapper.java @@ -0,0 +1,141 @@ +package com.dalab.policyengine.mapper; + +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import com.dalab.policyengine.dto.PolicyEvaluationOutputDTO; +import com.dalab.policyengine.dto.PolicyEvaluationSummaryDTO; +// Add missing DTO imports +import com.dalab.policyengine.dto.PolicyInputDTO; +import com.dalab.policyengine.dto.PolicyOutputDTO; +import com.dalab.policyengine.dto.PolicyRuleDTO; +import com.dalab.policyengine.dto.PolicySummaryDTO; +// Add missing entity imports +import com.dalab.policyengine.model.Policy; +import com.dalab.policyengine.model.PolicyEvaluation; +import com.dalab.policyengine.model.PolicyEvaluationStatus; +import com.dalab.policyengine.model.PolicyRule; + +@Component +public class PolicyMapper { + + public PolicySummaryDTO toPolicySummaryDTO(Policy policy) { + if (policy == null) return null; + PolicySummaryDTO dto = new PolicySummaryDTO(); + dto.setId(policy.getId()); + dto.setName(policy.getName()); + dto.setDescription(policy.getDescription()); + dto.setStatus(policy.getStatus()); + dto.setRuleCount(policy.getRules() != null ? policy.getRules().size() : 0); + dto.setCreatedAt(policy.getCreatedAt()); + dto.setUpdatedAt(policy.getUpdatedAt()); + return dto; + } + + public PolicyOutputDTO toPolicyOutputDTO(Policy policy) { + if (policy == null) return null; + PolicyOutputDTO dto = new PolicyOutputDTO(); + dto.setId(policy.getId()); + dto.setName(policy.getName()); + dto.setDescription(policy.getDescription()); + dto.setStatus(policy.getStatus()); + dto.setConditionLogic(policy.getConditionLogic()); + dto.setActions(policy.getActions()); + dto.setRules(policy.getRules().stream().map(this::toPolicyRuleDTO).collect(Collectors.toList())); + dto.setCreatedAt(policy.getCreatedAt()); + dto.setUpdatedAt(policy.getUpdatedAt()); + dto.setCreatedByUserId(policy.getCreatedByUserId()); + dto.setUpdatedByUserId(policy.getUpdatedByUserId()); + return dto; + } + + public PolicyRuleDTO toPolicyRuleDTO(PolicyRule policyRule) { + if (policyRule == null) return null; + PolicyRuleDTO dto = new PolicyRuleDTO(); + dto.setId(policyRule.getId() != null ? policyRule.getId().toString() : null); + dto.setName(policyRule.getName()); + dto.setDescription(policyRule.getDescription()); + dto.setCondition(policyRule.getCondition()); + dto.setPriority(policyRule.getPriority()); + dto.setActions(policyRule.getActions()); + return dto; + } + + public Policy toPolicyEntity(PolicyInputDTO dto) { + if (dto == null) return null; + Policy policy = new Policy(); + updatePolicyEntityFromInputDTO(policy, dto); // Reuse logic for update + return policy; + } + + public void updatePolicyEntityFromInputDTO(Policy policy, PolicyInputDTO dto) { + if (policy == null || dto == null) return; + + policy.setName(dto.getName()); + policy.setDescription(dto.getDescription()); + policy.setStatus(dto.getStatus()); + policy.setConditionLogic(dto.getConditionLogic()); + policy.setActions(dto.getActions()); + + // Handle rules update carefully + if (dto.getRules() != null) { + // Simple strategy: remove all existing and add new ones based on DTO + // More sophisticated merging could be done (e.g. based on rule ID or name) + policy.getRules().clear(); + dto.getRules().forEach(ruleDTO -> policy.addRule(toPolicyRuleEntity(ruleDTO))); + } else { + policy.setRules(Collections.emptyList()); + } + } + + public PolicyRule toPolicyRuleEntity(PolicyRuleDTO dto) { + if (dto == null) return null; + PolicyRule rule = new PolicyRule(); + // ID is not set here for new rules, or could be used to fetch existing if updating + // For simplicity, if ID is present in DTO, we assume it might be used by service layer + // to find and update existing rule, but mapper focuses on new entity creation or field mapping. + if (dto.getId() != null) { + try { + rule.setId(UUID.fromString(dto.getId())); + } catch (IllegalArgumentException e) { + // Handle invalid UUID string if necessary, or let it be null for new entity + } + } + rule.setName(dto.getName()); + rule.setDescription(dto.getDescription()); + rule.setCondition(dto.getCondition()); + rule.setPriority(dto.getPriority()); + rule.setActions(dto.getActions()); + return rule; + } + + public PolicyEvaluationOutputDTO toPolicyEvaluationOutputDTO(PolicyEvaluation evaluation, String policyName) { + if (evaluation == null) return null; + PolicyEvaluationOutputDTO dto = new PolicyEvaluationOutputDTO(); + dto.setId(evaluation.getId()); + dto.setPolicyId(evaluation.getPolicyId()); + dto.setPolicyName(policyName); // Policy name fetched separately for convenience + dto.setTargetAssetId(evaluation.getTargetAssetId()); + dto.setStatus(evaluation.getStatus()); + dto.setEvaluationDetails(evaluation.getEvaluationDetails()); + dto.setTriggeredActions(evaluation.getTriggeredActions()); + dto.setEvaluatedAt(evaluation.getEvaluatedAt()); + dto.setEvaluationTriggeredByUserId(evaluation.getEvaluationTriggeredByUserId()); + return dto; + } + + public PolicyEvaluationSummaryDTO toPolicyEvaluationSummaryDTO(PolicyEvaluation evaluation, String policyName) { + if (evaluation == null) return null; + PolicyEvaluationSummaryDTO dto = new PolicyEvaluationSummaryDTO(); + dto.setId(evaluation.getId()); + dto.setPolicyId(evaluation.getPolicyId()); + dto.setPolicyName(policyName); + dto.setTargetAssetId(evaluation.getTargetAssetId()); + dto.setStatus(evaluation.getStatus()); + dto.setEvaluatedAt(evaluation.getEvaluatedAt()); + return dto; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/EventRule.java b/src/main/java/com/dalab/policyengine/model/EventRule.java new file mode 100644 index 0000000000000000000000000000000000000000..1f5c102cc11c2ca857566ab683d0a38b9312a5b0 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/EventRule.java @@ -0,0 +1,226 @@ +package com.dalab.policyengine.model; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * Entity representing a filtering rule for event subscriptions. + * Uses MVEL expressions to define conditions for event matching. + */ +@Entity +@Table(name = "event_rules") +public class EventRule { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @NotBlank + @Size(max = 255) + @Column(nullable = false) + private String name; + + @Size(max = 500) + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "subscription_id", nullable = false) + private EventSubscription subscription; + + /** + * MVEL expression for evaluating events + * Example: "eventType == 'POLICY_VIOLATION' && severity == 'HIGH' && assetId.startsWith('prod-')" + */ + @Column(columnDefinition = "TEXT", nullable = false) + private String condition; + + /** + * Rule priority (lower number = higher priority) + */ + @Column(nullable = false) + private Integer priority = 1; + + /** + * Whether this rule is enabled + */ + @Column(nullable = false) + private Boolean enabled = true; + + /** + * Additional parameters for the rule + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map parameters; + + @Column(nullable = false, updatable = false) + private Instant createdAt; + + private Instant updatedAt; + + @Column(columnDefinition = "UUID") + private UUID createdByUserId; + + @Column(columnDefinition = "UUID") + private UUID updatedByUserId; + + // Constructors + public EventRule() {} + + public EventRule(String name, String condition) { + this.name = name; + this.condition = condition; + } + + // Getters and Setters + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public EventSubscription getSubscription() { + return subscription; + } + + public void setSubscription(EventSubscription subscription) { + this.subscription = subscription; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } + + @PrePersist + protected void onCreate() { + createdAt = Instant.now(); + updatedAt = Instant.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = Instant.now(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EventRule)) return false; + EventRule eventRule = (EventRule) o; + return id != null && id.equals(eventRule.getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String toString() { + return "EventRule{" + + "id=" + id + + ", name='" + name + '\'' + + ", condition='" + condition + '\'' + + ", priority=" + priority + + ", enabled=" + enabled + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/EventSeverity.java b/src/main/java/com/dalab/policyengine/model/EventSeverity.java new file mode 100644 index 0000000000000000000000000000000000000000..2872064e153a652395a1aae3a7450bb34f50c6ce --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/EventSeverity.java @@ -0,0 +1,31 @@ +package com.dalab.policyengine.model; + +/** + * Enumeration representing the severity levels of events in the DALab platform. + */ +public enum EventSeverity { + /** + * Critical severity - immediate attention required + */ + CRITICAL, + + /** + * High severity - urgent attention required + */ + HIGH, + + /** + * Medium severity - attention required + */ + MEDIUM, + + /** + * Low severity - informational + */ + LOW, + + /** + * Informational - for logging and tracking purposes + */ + INFO +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/EventSubscription.java b/src/main/java/com/dalab/policyengine/model/EventSubscription.java new file mode 100644 index 0000000000000000000000000000000000000000..b0b1dbc53ac05fa9306573325f97ffae1ec5c345 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/EventSubscription.java @@ -0,0 +1,339 @@ +package com.dalab.policyengine.model; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * Entity representing an event subscription configuration. + * Users can subscribe to specific types of events and configure + * notification preferences and action triggers. + */ +@Entity +@Table(name = "event_subscriptions") +public class EventSubscription { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @NotBlank + @Size(max = 255) + @Column(nullable = false) + private String name; + + @Size(max = 1000) + private String description; + + @NotNull + @Column(columnDefinition = "UUID", nullable = false) + private UUID userId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private EventSubscriptionStatus status = EventSubscriptionStatus.ACTIVE; + + /** + * Event types to subscribe to (e.g., POLICY_VIOLATION, ASSET_DISCOVERED, COMPLIANCE_ISSUE) + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "event_subscription_types", joinColumns = @JoinColumn(name = "subscription_id")) + @Enumerated(EnumType.STRING) + @Column(name = "event_type") + private List eventTypes = new ArrayList<>(); + + /** + * Event severity levels to include (e.g., HIGH, MEDIUM, LOW) + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "event_subscription_severities", joinColumns = @JoinColumn(name = "subscription_id")) + @Enumerated(EnumType.STRING) + @Column(name = "severity") + private List severities = new ArrayList<>(); + + /** + * Source services to monitor (e.g., da-catalog, da-discovery, da-compliance) + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "event_subscription_sources", joinColumns = @JoinColumn(name = "subscription_id")) + @Column(name = "source_service") + private List sourceServices = new ArrayList<>(); + + /** + * Filter conditions for events (MVEL expressions) + */ + @OneToMany(mappedBy = "subscription", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private List rules = new ArrayList<>(); + + /** + * Notification preferences and action configurations + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map notificationConfig; // e.g., { "email": true, "slack": { "channel": "#alerts" } } + + /** + * Action configurations to trigger when events match + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map actionConfig; // e.g., { "autoQuarantine": true, "escalateTo": "admin" } + + @Column(nullable = false, updatable = false) + private Instant createdAt; + + private Instant updatedAt; + + @Column(columnDefinition = "UUID") + private UUID createdByUserId; + + @Column(columnDefinition = "UUID") + private UUID updatedByUserId; + + // Constructors + public EventSubscription() {} + + public EventSubscription(String name, UUID userId) { + this.name = name; + this.userId = userId; + } + + // Getters and Setters + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public UUID getUserId() { + return userId; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public EventSubscriptionStatus getStatus() { + return status; + } + + public void setStatus(EventSubscriptionStatus status) { + this.status = status; + } + + public List getEventTypes() { + return eventTypes; + } + + public void setEventTypes(List eventTypes) { + this.eventTypes = eventTypes != null ? eventTypes : new ArrayList<>(); + } + + public void addEventType(EventType eventType) { + if (this.eventTypes == null) { + this.eventTypes = new ArrayList<>(); + } + this.eventTypes.add(eventType); + } + + public void removeEventType(EventType eventType) { + if (this.eventTypes != null) { + this.eventTypes.remove(eventType); + } + } + + public List getSeverities() { + return severities; + } + + public void setSeverities(List severities) { + this.severities = severities != null ? severities : new ArrayList<>(); + } + + public void addSeverity(EventSeverity severity) { + if (this.severities == null) { + this.severities = new ArrayList<>(); + } + this.severities.add(severity); + } + + public void removeSeverity(EventSeverity severity) { + if (this.severities != null) { + this.severities.remove(severity); + } + } + + public List getSourceServices() { + return sourceServices; + } + + public void setSourceServices(List sourceServices) { + this.sourceServices = sourceServices != null ? sourceServices : new ArrayList<>(); + } + + public void addSourceService(String sourceService) { + if (this.sourceServices == null) { + this.sourceServices = new ArrayList<>(); + } + this.sourceServices.add(sourceService); + } + + public void removeSourceService(String sourceService) { + if (this.sourceServices != null) { + this.sourceServices.remove(sourceService); + } + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules != null ? rules : new ArrayList<>(); + this.rules.forEach(rule -> rule.setSubscription(this)); + } + + public void addRule(EventRule rule) { + if (this.rules == null) { + this.rules = new ArrayList<>(); + } + this.rules.add(rule); + rule.setSubscription(this); + } + + public void removeRule(EventRule rule) { + if (this.rules != null) { + this.rules.remove(rule); + rule.setSubscription(null); + } + } + + public Map getNotificationConfig() { + return notificationConfig; + } + + public void setNotificationConfig(Map notificationConfig) { + this.notificationConfig = notificationConfig; + } + + public Map getActionConfig() { + return actionConfig; + } + + public void setActionConfig(Map actionConfig) { + this.actionConfig = actionConfig; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } + + @PrePersist + protected void onCreate() { + createdAt = Instant.now(); + updatedAt = Instant.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = Instant.now(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EventSubscription)) return false; + EventSubscription that = (EventSubscription) o; + return id != null && id.equals(that.getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String toString() { + return "EventSubscription{" + + "id=" + id + + ", name='" + name + '\'' + + ", userId=" + userId + + ", status=" + status + + ", eventTypes=" + eventTypes + + ", severities=" + severities + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/EventSubscriptionStatus.java b/src/main/java/com/dalab/policyengine/model/EventSubscriptionStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..f9c9291b63005ee22a07c7b543c29b1a753863b9 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/EventSubscriptionStatus.java @@ -0,0 +1,26 @@ +package com.dalab.policyengine.model; + +/** + * Enumeration representing the status of an event subscription. + */ +public enum EventSubscriptionStatus { + /** + * Subscription is active and receiving events + */ + ACTIVE, + + /** + * Subscription is temporarily paused + */ + PAUSED, + + /** + * Subscription is disabled and not receiving events + */ + DISABLED, + + /** + * Subscription has been archived + */ + ARCHIVED +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/EventType.java b/src/main/java/com/dalab/policyengine/model/EventType.java new file mode 100644 index 0000000000000000000000000000000000000000..cd3f578e6ba486ad8efff0c728352a6f7b36c2ce --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/EventType.java @@ -0,0 +1,123 @@ +package com.dalab.policyengine.model; + +/** + * Enumeration representing different types of events that can occur in the DALab platform. + */ +public enum EventType { + // Policy-related events + /** + * Policy violation detected + */ + POLICY_VIOLATION, + + /** + * Policy evaluation completed + */ + POLICY_EVALUATION_COMPLETED, + + /** + * Policy applied to assets + */ + POLICY_APPLIED, + + /** + * Policy rule triggered + */ + POLICY_RULE_TRIGGERED, + + // Asset-related events + /** + * New asset discovered + */ + ASSET_DISCOVERED, + + /** + * Asset metadata changed + */ + ASSET_METADATA_CHANGED, + + /** + * Asset classification changed + */ + ASSET_CLASSIFICATION_CHANGED, + + /** + * Asset access pattern detected + */ + ASSET_ACCESS_DETECTED, + + // Compliance-related events + /** + * Compliance violation detected + */ + COMPLIANCE_VIOLATION, + + /** + * Compliance scan completed + */ + COMPLIANCE_SCAN_COMPLETED, + + /** + * Regulatory requirement changed + */ + REGULATORY_CHANGE, + + // Data lifecycle events + /** + * Asset scheduled for archival + */ + ASSET_ARCHIVAL_SCHEDULED, + + /** + * Asset archival completed + */ + ASSET_ARCHIVAL_COMPLETED, + + /** + * Asset deletion scheduled + */ + ASSET_DELETION_SCHEDULED, + + /** + * Asset deletion completed + */ + ASSET_DELETION_COMPLETED, + + // System events + /** + * System alert generated + */ + SYSTEM_ALERT, + + /** + * Service health check failed + */ + SERVICE_HEALTH_ISSUE, + + /** + * Data quality issue detected + */ + DATA_QUALITY_ISSUE, + + /** + * Security incident detected + */ + SECURITY_INCIDENT, + + // User activity events + /** + * User access pattern anomaly + */ + USER_ACCESS_ANOMALY, + + /** + * Unauthorized access attempt + */ + UNAUTHORIZED_ACCESS_ATTEMPT, + + // Generic event type for custom events + /** + * Custom event type + */ + CUSTOM_EVENT +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/Policy.java b/src/main/java/com/dalab/policyengine/model/Policy.java new file mode 100644 index 0000000000000000000000000000000000000000..b626b34ec9b3f7630251c99c3db72a181734dfd5 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/Policy.java @@ -0,0 +1,171 @@ +package com.dalab.policyengine.model; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +@Entity +@Table(name = "policies") +public class Policy { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @NotBlank + @Size(max = 255) + @Column(nullable = false, unique = true) + private String name; + + @Size(max = 1000) + private String description; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PolicyStatus status = PolicyStatus.DISABLED; + + // MVEL condition for the policy. If all rules pass, this condition is evaluated. + // Could be null if policy relies solely on its individual rules. + @Column(columnDefinition = "TEXT") + private String conditionLogic; // e.g., "rule1 && (rule2 || rule3)" + + @OneToMany(mappedBy = "policy", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private List rules = new ArrayList<>(); + + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map actions; // e.g., { "notify": { "email": "admin@example.com" }, "addLabel": "Sensitive" } + + @Column(nullable = false, updatable = false) + private Instant createdAt; + + private Instant updatedAt; + + @Column(columnDefinition = "UUID") + private UUID createdByUserId; + + @Column(columnDefinition = "UUID") + private UUID updatedByUserId; + + // Getters and Setters + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public PolicyStatus getStatus() { + return status; + } + + public void setStatus(PolicyStatus status) { + this.status = status; + } + + public String getConditionLogic() { + return conditionLogic; + } + + public void setConditionLogic(String conditionLogic) { + this.conditionLogic = conditionLogic; + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules; + this.rules.forEach(rule -> rule.setPolicy(this)); + } + + public void addRule(PolicyRule rule) { + this.rules.add(rule); + rule.setPolicy(this); + } + + public void removeRule(PolicyRule rule) { + this.rules.remove(rule); + rule.setPolicy(null); + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } + + @PrePersist + protected void onCreate() { + createdAt = Instant.now(); + updatedAt = Instant.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = Instant.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/PolicyDraft.java b/src/main/java/com/dalab/policyengine/model/PolicyDraft.java new file mode 100644 index 0000000000000000000000000000000000000000..dd3a7ecd6aba0e09bbe135a58f45dedf9d559072 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/PolicyDraft.java @@ -0,0 +1,495 @@ +package com.dalab.policyengine.model; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * Entity representing a policy draft that goes through approval workflow. + * Supports versioning, collaboration, and audit trails. + */ +@Entity +@Table(name = "policy_drafts") +public class PolicyDraft { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + /** + * Name of the policy (must be unique when published). + */ + @NotBlank + @Size(max = 255) + @Column(nullable = false) + private String name; + + /** + * Detailed description of the policy's purpose and scope. + */ + @Size(max = 2000) + @Column(columnDefinition = "TEXT") + private String description; + + /** + * Current status in the approval workflow. + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PolicyDraftStatus status = PolicyDraftStatus.CREATED; + + /** + * Version number for this draft (incremented on each revision). + */ + @Column(nullable = false) + private Integer version = 1; + + /** + * Reference to the published policy (if this is an update to existing policy). + */ + @Column(columnDefinition = "UUID") + private UUID basePolicyId; + + /** + * MVEL condition logic for the policy evaluation. + */ + @Column(columnDefinition = "TEXT") + private String conditionLogic; + + /** + * JSON representation of policy rules structure. + * Stored as JSON to support complex rule configurations. + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private List> rulesDefinition; + + /** + * JSON representation of actions to be taken when policy is triggered. + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map actions; + + /** + * Change summary describing what was modified in this version. + */ + @Size(max = 1000) + @Column(columnDefinition = "TEXT") + private String changeSummary; + + /** + * Justification for the policy changes or creation. + */ + @Size(max = 2000) + @Column(columnDefinition = "TEXT") + private String justification; + + /** + * Expected impact of implementing this policy. + */ + @Size(max = 1000) + @Column(columnDefinition = "TEXT") + private String expectedImpact; + + /** + * Target implementation date for the policy. + */ + private Instant targetImplementationDate; + + /** + * Priority level for policy implementation (HIGH, MEDIUM, LOW). + */ + @Size(max = 50) + private String priority = "MEDIUM"; + + /** + * Business category or domain this policy applies to. + */ + @Size(max = 100) + private String category; + + /** + * Tags for categorization and searchability. + */ + @ElementCollection + @CollectionTable(name = "policy_draft_tags", joinColumns = @JoinColumn(name = "draft_id")) + @Column(name = "tag") + private List tags = new ArrayList<>(); + + /** + * Stakeholders who should be notified about this policy. + */ + @ElementCollection + @CollectionTable(name = "policy_draft_stakeholders", joinColumns = @JoinColumn(name = "draft_id")) + @Column(name = "stakeholder_id", columnDefinition = "UUID") + private List stakeholders = new ArrayList<>(); + + /** + * Approval workflow metadata. + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map approvalMetadata; + + /** + * Reviewer comments and feedback. + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private List> reviewComments = new ArrayList<>(); + + // Audit fields + @Column(nullable = false, updatable = false) + private Instant createdAt; + + private Instant updatedAt; + + @Column(columnDefinition = "UUID", nullable = false) + private UUID createdByUserId; + + @Column(columnDefinition = "UUID") + private UUID updatedByUserId; + + private Instant submittedAt; + + @Column(columnDefinition = "UUID") + private UUID submittedByUserId; + + private Instant approvedAt; + + @Column(columnDefinition = "UUID") + private UUID approvedByUserId; + + private Instant rejectedAt; + + @Column(columnDefinition = "UUID") + private UUID rejectedByUserId; + + private Instant publishedAt; + + @Column(columnDefinition = "UUID") + private UUID publishedByUserId; + + // Constructors + public PolicyDraft() {} + + public PolicyDraft(String name, String description, UUID createdByUserId) { + this.name = name; + this.description = description; + this.createdByUserId = createdByUserId; + } + + // Getters and Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public PolicyDraftStatus getStatus() { + return status; + } + + public void setStatus(PolicyDraftStatus status) { + this.status = status; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public UUID getBasePolicyId() { + return basePolicyId; + } + + public void setBasePolicyId(UUID basePolicyId) { + this.basePolicyId = basePolicyId; + } + + public String getConditionLogic() { + return conditionLogic; + } + + public void setConditionLogic(String conditionLogic) { + this.conditionLogic = conditionLogic; + } + + public List> getRulesDefinition() { + return rulesDefinition; + } + + public void setRulesDefinition(List> rulesDefinition) { + this.rulesDefinition = rulesDefinition; + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } + + public String getChangeSummary() { + return changeSummary; + } + + public void setChangeSummary(String changeSummary) { + this.changeSummary = changeSummary; + } + + public String getJustification() { + return justification; + } + + public void setJustification(String justification) { + this.justification = justification; + } + + public String getExpectedImpact() { + return expectedImpact; + } + + public void setExpectedImpact(String expectedImpact) { + this.expectedImpact = expectedImpact; + } + + public Instant getTargetImplementationDate() { + return targetImplementationDate; + } + + public void setTargetImplementationDate(Instant targetImplementationDate) { + this.targetImplementationDate = targetImplementationDate; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public List getStakeholders() { + return stakeholders; + } + + public void setStakeholders(List stakeholders) { + this.stakeholders = stakeholders; + } + + public Map getApprovalMetadata() { + return approvalMetadata; + } + + public void setApprovalMetadata(Map approvalMetadata) { + this.approvalMetadata = approvalMetadata; + } + + public List> getReviewComments() { + return reviewComments; + } + + public void setReviewComments(List> reviewComments) { + this.reviewComments = reviewComments; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public UUID getCreatedByUserId() { + return createdByUserId; + } + + public void setCreatedByUserId(UUID createdByUserId) { + this.createdByUserId = createdByUserId; + } + + public UUID getUpdatedByUserId() { + return updatedByUserId; + } + + public void setUpdatedByUserId(UUID updatedByUserId) { + this.updatedByUserId = updatedByUserId; + } + + public Instant getSubmittedAt() { + return submittedAt; + } + + public void setSubmittedAt(Instant submittedAt) { + this.submittedAt = submittedAt; + } + + public UUID getSubmittedByUserId() { + return submittedByUserId; + } + + public void setSubmittedByUserId(UUID submittedByUserId) { + this.submittedByUserId = submittedByUserId; + } + + public Instant getApprovedAt() { + return approvedAt; + } + + public void setApprovedAt(Instant approvedAt) { + this.approvedAt = approvedAt; + } + + public UUID getApprovedByUserId() { + return approvedByUserId; + } + + public void setApprovedByUserId(UUID approvedByUserId) { + this.approvedByUserId = approvedByUserId; + } + + public Instant getRejectedAt() { + return rejectedAt; + } + + public void setRejectedAt(Instant rejectedAt) { + this.rejectedAt = rejectedAt; + } + + public UUID getRejectedByUserId() { + return rejectedByUserId; + } + + public void setRejectedByUserId(UUID rejectedByUserId) { + this.rejectedByUserId = rejectedByUserId; + } + + public Instant getPublishedAt() { + return publishedAt; + } + + public void setPublishedAt(Instant publishedAt) { + this.publishedAt = publishedAt; + } + + public UUID getPublishedByUserId() { + return publishedByUserId; + } + + public void setPublishedByUserId(UUID publishedByUserId) { + this.publishedByUserId = publishedByUserId; + } + + /** + * Utility method to add a review comment. + */ + public void addReviewComment(String comment, UUID reviewerId, String reviewerRole) { + Map commentData = Map.of( + "comment", comment, + "reviewerId", reviewerId.toString(), + "reviewerRole", reviewerRole, + "timestamp", Instant.now().toString() + ); + this.reviewComments.add(commentData); + } + + /** + * Utility method to add a tag if not already present. + */ + public void addTag(String tag) { + if (!this.tags.contains(tag)) { + this.tags.add(tag); + } + } + + /** + * Utility method to add a stakeholder if not already present. + */ + public void addStakeholder(UUID stakeholderId) { + if (!this.stakeholders.contains(stakeholderId)) { + this.stakeholders.add(stakeholderId); + } + } + + @PrePersist + protected void onCreate() { + createdAt = Instant.now(); + updatedAt = Instant.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = Instant.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/PolicyDraftStatus.java b/src/main/java/com/dalab/policyengine/model/PolicyDraftStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..ff1e21725ac50d5311fd4e9db62b720a0129235a --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/PolicyDraftStatus.java @@ -0,0 +1,47 @@ +package com.dalab.policyengine.model; + +/** + * Status enumeration for policy drafts supporting complete workflow lifecycle. + * Tracks the draft from creation through approval and publication. + */ +public enum PolicyDraftStatus { + /** + * Draft has been created but not yet submitted for review. + */ + CREATED, + + /** + * Draft has been submitted and is awaiting review. + */ + SUBMITTED, + + /** + * Draft is currently under review by approvers. + */ + UNDER_REVIEW, + + /** + * Draft requires changes based on reviewer feedback. + */ + REQUIRES_CHANGES, + + /** + * Draft has been approved and is ready for publication. + */ + APPROVED, + + /** + * Draft has been rejected and will not be published. + */ + REJECTED, + + /** + * Draft has been published as an active policy. + */ + PUBLISHED, + + /** + * Draft has been archived (no longer active). + */ + ARCHIVED +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/PolicyEvaluation.java b/src/main/java/com/dalab/policyengine/model/PolicyEvaluation.java new file mode 100644 index 0000000000000000000000000000000000000000..baa5e10b237047ba1b47f00cc7d672c9c494675a --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/PolicyEvaluation.java @@ -0,0 +1,119 @@ +package com.dalab.policyengine.model; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.*; + +@Entity +@Table(name = "policy_evaluations") +public class PolicyEvaluation { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @Column(nullable = false, columnDefinition = "UUID") + private UUID policyId; + + @Column(nullable = false, length = 255) // External asset ID from da-catalog or other source + private String targetAssetId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PolicyEvaluationStatus status; + + // Store details about the evaluation, e.g., which rules passed/failed, input facts snapshot + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map evaluationDetails; + + // Store actions that were triggered as a result of this evaluation + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map triggeredActions; + + @Column(nullable = false) + private Instant evaluatedAt; + + @Column(columnDefinition = "UUID") + private UUID evaluationTriggeredByUserId; // Optional: if triggered by a user action + + // Getters and Setters + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getPolicyId() { + return policyId; + } + + public void setPolicyId(UUID policyId) { + this.policyId = policyId; + } + + public String getTargetAssetId() { + return targetAssetId; + } + + public void setTargetAssetId(String targetAssetId) { + this.targetAssetId = targetAssetId; + } + + public PolicyEvaluationStatus getStatus() { + return status; + } + + public void setStatus(PolicyEvaluationStatus status) { + this.status = status; + } + + public Map getEvaluationDetails() { + return evaluationDetails; + } + + public void setEvaluationDetails(Map evaluationDetails) { + this.evaluationDetails = evaluationDetails; + } + + public Map getTriggeredActions() { + return triggeredActions; + } + + public void setTriggeredActions(Map triggeredActions) { + this.triggeredActions = triggeredActions; + } + + public Instant getEvaluatedAt() { + return evaluatedAt; + } + + public void setEvaluatedAt(Instant evaluatedAt) { + this.evaluatedAt = evaluatedAt; + } + + public UUID getEvaluationTriggeredByUserId() { + return evaluationTriggeredByUserId; + } + + public void setEvaluationTriggeredByUserId(UUID evaluationTriggeredByUserId) { + this.evaluationTriggeredByUserId = evaluationTriggeredByUserId; + } + + @PrePersist + protected void onPersist() { + if (evaluatedAt == null) { + evaluatedAt = Instant.now(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/PolicyEvaluationStatus.java b/src/main/java/com/dalab/policyengine/model/PolicyEvaluationStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..57ad0dd2f327dc23ada8cb6239e5985b4126c168 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/PolicyEvaluationStatus.java @@ -0,0 +1,9 @@ +package com.dalab.policyengine.model; + +public enum PolicyEvaluationStatus { + PASS, // Policy conditions met, actions may have been triggered + FAIL, // Policy conditions not met + ERROR, // Error during evaluation (e.g., bad rule syntax, fact unavailable) + PENDING, // Evaluation is queued or in progress + NOT_APPLICABLE // Policy was not applicable to the target asset +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/PolicyRule.java b/src/main/java/com/dalab/policyengine/model/PolicyRule.java new file mode 100644 index 0000000000000000000000000000000000000000..9c3e9a21fce9f90297b7123d6703cb22b9b9d83b --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/PolicyRule.java @@ -0,0 +1,137 @@ +package com.dalab.policyengine.model; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +@Entity +@Table(name = "policy_rules") +public class PolicyRule { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @NotBlank + @Size(max = 255) + @Column(nullable = false) + private String name; // A unique name for the rule within the policy, e.g., "rule1", "checkPII" + + @Size(max = 1000) + private String description; + + @NotBlank + @Column(nullable = false, columnDefinition = "TEXT") + private String condition; // MVEL expression, e.g., "asset.assetType == 'S3_BUCKET' && asset.tags.contains('PII')" + + // Rules with lower numbers have higher priority + @Column(nullable = false) + private int priority = 1; + + // Optional: Actions specific to this rule, if different from policy-level actions or to augment them + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map actions; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "policy_id", nullable = false) + private Policy policy; + + @Column(nullable = false, updatable = false) + private Instant createdAt; + + private Instant updatedAt; + + // Getters and Setters + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public Map getActions() { + return actions; + } + + public void setActions(Map actions) { + this.actions = actions; + } + + public Policy getPolicy() { + return policy; + } + + public void setPolicy(Policy policy) { + this.policy = policy; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + @PrePersist + protected void onCreate() { + createdAt = Instant.now(); + updatedAt = Instant.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = Instant.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/model/PolicyStatus.java b/src/main/java/com/dalab/policyengine/model/PolicyStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..8beec86bcc07968276a4ef0588d61b05c8c6174e --- /dev/null +++ b/src/main/java/com/dalab/policyengine/model/PolicyStatus.java @@ -0,0 +1,6 @@ +package com.dalab.policyengine.model; + +public enum PolicyStatus { + ENABLED, + DISABLED +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/repository/EventRuleRepository.java b/src/main/java/com/dalab/policyengine/repository/EventRuleRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..44989d62a357f9ea5203d7b1ec622d5519560140 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/repository/EventRuleRepository.java @@ -0,0 +1,86 @@ +package com.dalab.policyengine.repository; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.dalab.policyengine.model.EventRule; + +/** + * Repository interface for EventRule entities. + * Provides data access methods for event rule management. + */ +@Repository +public interface EventRuleRepository extends JpaRepository { + + /** + * Find all rules for a specific subscription + */ + List findBySubscriptionId(UUID subscriptionId); + + /** + * Find enabled rules for a specific subscription ordered by priority + */ + List findBySubscriptionIdAndEnabledTrueOrderByPriorityAsc(UUID subscriptionId); + + /** + * Find rules by name pattern + */ + List findByNameContainingIgnoreCase(String namePattern); + + /** + * Find rules for a subscription by name pattern + */ + List findBySubscriptionIdAndNameContainingIgnoreCase(UUID subscriptionId, String namePattern); + + /** + * Check if a rule with the given name already exists for a subscription + */ + boolean existsBySubscriptionIdAndName(UUID subscriptionId, String name); + + /** + * Find rules by enabled status + */ + List findByEnabled(Boolean enabled); + + /** + * Find rules with a specific priority + */ + List findByPriority(Integer priority); + + /** + * Find rules created by a specific user + */ + List findByCreatedByUserId(UUID createdByUserId); + + /** + * Count rules for a specific subscription + */ + long countBySubscriptionId(UUID subscriptionId); + + /** + * Count enabled rules for a specific subscription + */ + long countBySubscriptionIdAndEnabledTrue(UUID subscriptionId); + + /** + * Find rules that contain a specific condition pattern + */ + @Query("SELECT er FROM EventRule er WHERE er.condition LIKE %:conditionPattern%") + List findByConditionContaining(@Param("conditionPattern") String conditionPattern); + + /** + * Find the highest priority rule for a subscription + */ + @Query("SELECT er FROM EventRule er WHERE er.subscription.id = :subscriptionId AND er.enabled = true ORDER BY er.priority ASC LIMIT 1") + EventRule findHighestPriorityEnabledRule(@Param("subscriptionId") UUID subscriptionId); + + /** + * Find rules for a subscription ordered by priority + */ + List findBySubscriptionIdOrderByPriorityAsc(UUID subscriptionId); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/repository/EventSubscriptionRepository.java b/src/main/java/com/dalab/policyengine/repository/EventSubscriptionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..d2d86d7311fef30d3a8b72bc72cf8c976dde1874 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/repository/EventSubscriptionRepository.java @@ -0,0 +1,100 @@ +package com.dalab.policyengine.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.dalab.policyengine.model.EventSubscription; +import com.dalab.policyengine.model.EventSubscriptionStatus; +import com.dalab.policyengine.model.EventType; + +/** + * Repository interface for EventSubscription entities. + * Provides data access methods for event subscription management. + */ +@Repository +public interface EventSubscriptionRepository extends JpaRepository { + + /** + * Find all event subscriptions for a specific user + */ + List findByUserId(UUID userId); + + /** + * Find event subscriptions by user ID with pagination + */ + Page findByUserId(UUID userId, Pageable pageable); + + /** + * Find event subscriptions by status + */ + List findByStatus(EventSubscriptionStatus status); + + /** + * Find active event subscriptions for a specific user + */ + List findByUserIdAndStatus(UUID userId, EventSubscriptionStatus status); + + /** + * Find event subscriptions that monitor a specific event type + */ + @Query("SELECT es FROM EventSubscription es JOIN es.eventTypes et WHERE et = :eventType AND es.status = 'ACTIVE'") + List findActiveSubscriptionsByEventType(@Param("eventType") EventType eventType); + + /** + * Find event subscriptions that monitor a specific source service + */ + @Query("SELECT es FROM EventSubscription es JOIN es.sourceServices ss WHERE ss = :sourceService AND es.status = 'ACTIVE'") + List findActiveSubscriptionsBySourceService(@Param("sourceService") String sourceService); + + /** + * Find event subscriptions by name pattern + */ + List findByNameContainingIgnoreCase(String namePattern); + + /** + * Find event subscriptions for a user by name pattern + */ + List findByUserIdAndNameContainingIgnoreCase(UUID userId, String namePattern); + + /** + * Check if a subscription with the given name already exists for a user + */ + boolean existsByUserIdAndName(UUID userId, String name); + + /** + * Find active subscriptions that could match an event based on event type and source service + */ + @Query("SELECT DISTINCT es FROM EventSubscription es " + + "LEFT JOIN es.eventTypes et " + + "LEFT JOIN es.sourceServices ss " + + "WHERE es.status = 'ACTIVE' " + + "AND (et = :eventType OR es.eventTypes IS EMPTY) " + + "AND (ss = :sourceService OR es.sourceServices IS EMPTY)") + List findPotentialMatchingSubscriptions( + @Param("eventType") EventType eventType, + @Param("sourceService") String sourceService); + + /** + * Count active subscriptions for a user + */ + long countByUserIdAndStatus(UUID userId, EventSubscriptionStatus status); + + /** + * Find subscriptions created by a specific user + */ + List findByCreatedByUserId(UUID createdByUserId); + + /** + * Find subscriptions with a specific notification type enabled + */ + @Query("SELECT es FROM EventSubscription es WHERE jsonb_extract_path_text(CAST(es.notificationConfig AS text), :notificationType) = 'true'") + List findSubscriptionsWithNotificationType(@Param("notificationType") String notificationType); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/repository/PolicyDraftRepository.java b/src/main/java/com/dalab/policyengine/repository/PolicyDraftRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..d9a1aaa2de395c8634625d259e23303792e4295b --- /dev/null +++ b/src/main/java/com/dalab/policyengine/repository/PolicyDraftRepository.java @@ -0,0 +1,169 @@ +package com.dalab.policyengine.repository; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.dalab.policyengine.model.PolicyDraft; +import com.dalab.policyengine.model.PolicyDraftStatus; + +/** + * Repository interface for PolicyDraft entities. + * Provides comprehensive query methods for draft management and workflow operations. + */ +@Repository +public interface PolicyDraftRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * Find all drafts by their current status. + */ + Page findByStatus(PolicyDraftStatus status, Pageable pageable); + + /** + * Find all drafts created by a specific user. + */ + Page findByCreatedByUserId(UUID createdByUserId, Pageable pageable); + + /** + * Find all drafts where the user is a stakeholder. + */ + @Query("SELECT pd FROM PolicyDraft pd WHERE :userId MEMBER OF pd.stakeholders") + Page findByStakeholder(@Param("userId") UUID userId, Pageable pageable); + + /** + * Find all drafts pending review (submitted or under review). + */ + @Query("SELECT pd FROM PolicyDraft pd WHERE pd.status IN ('SUBMITTED', 'UNDER_REVIEW')") + Page findPendingReview(Pageable pageable); + + /** + * Find all drafts that require attention from a specific user (created by them or they are stakeholder). + */ + @Query("SELECT DISTINCT pd FROM PolicyDraft pd WHERE pd.createdByUserId = :userId OR :userId MEMBER OF pd.stakeholders") + Page findRequiringAttention(@Param("userId") UUID userId, Pageable pageable); + + /** + * Find drafts by name containing specific text (case insensitive). + */ + Page findByNameContainingIgnoreCase(String name, Pageable pageable); + + /** + * Find drafts by category. + */ + Page findByCategory(String category, Pageable pageable); + + /** + * Find drafts by priority level. + */ + Page findByPriority(String priority, Pageable pageable); + + /** + * Find drafts that are based on a specific policy (updates to existing policies). + */ + List findByBasePolicyId(UUID basePolicyId); + + /** + * Find the latest version of drafts for a specific base policy. + */ + @Query("SELECT pd FROM PolicyDraft pd WHERE pd.basePolicyId = :basePolicyId ORDER BY pd.version DESC") + List findLatestVersionByBasePolicyId(@Param("basePolicyId") UUID basePolicyId); + + /** + * Find drafts with target implementation date before specified date. + */ + List findByTargetImplementationDateBefore(Instant date); + + /** + * Find drafts created within a specific time range. + */ + Page findByCreatedAtBetween(Instant startDate, Instant endDate, Pageable pageable); + + /** + * Find drafts that have been in a specific status for longer than specified time. + */ + @Query("SELECT pd FROM PolicyDraft pd WHERE pd.status = :status AND pd.updatedAt < :cutoffTime") + List findStaleInStatus(@Param("status") PolicyDraftStatus status, @Param("cutoffTime") Instant cutoffTime); + + /** + * Find drafts containing specific tags. + */ + @Query("SELECT pd FROM PolicyDraft pd WHERE :tag MEMBER OF pd.tags") + Page findByTag(@Param("tag") String tag, Pageable pageable); + + /** + * Find drafts by multiple criteria (complex search). + */ + @Query("SELECT pd FROM PolicyDraft pd WHERE " + + "(:status IS NULL OR pd.status = :status) AND " + + "(:category IS NULL OR pd.category = :category) AND " + + "(:priority IS NULL OR pd.priority = :priority) AND " + + "(:createdBy IS NULL OR pd.createdByUserId = :createdBy) AND " + + "(:nameFilter IS NULL OR LOWER(pd.name) LIKE LOWER(CONCAT('%', :nameFilter, '%')))") + Page findByCriteria( + @Param("status") PolicyDraftStatus status, + @Param("category") String category, + @Param("priority") String priority, + @Param("createdBy") UUID createdBy, + @Param("nameFilter") String nameFilter, + Pageable pageable + ); + + /** + * Count drafts by status. + */ + long countByStatus(PolicyDraftStatus status); + + /** + * Count drafts created by a specific user. + */ + long countByCreatedByUserId(UUID createdByUserId); + + /** + * Check if a draft name already exists (for validation). + */ + boolean existsByName(String name); + + /** + * Check if a draft name exists excluding a specific draft (for updates). + */ + @Query("SELECT CASE WHEN COUNT(pd) > 0 THEN true ELSE false END FROM PolicyDraft pd WHERE pd.name = :name AND pd.id != :excludeId") + boolean existsByNameExcludingId(@Param("name") String name, @Param("excludeId") UUID excludeId); + + /** + * Find all distinct categories used in drafts. + */ + @Query("SELECT DISTINCT pd.category FROM PolicyDraft pd WHERE pd.category IS NOT NULL") + List findAllCategories(); + + /** + * Find all distinct tags used in drafts. + */ + @Query("SELECT DISTINCT tag FROM PolicyDraft pd JOIN pd.tags tag") + List findAllTags(); + + /** + * Get draft statistics for dashboard. + */ + @Query("SELECT pd.status, COUNT(pd) FROM PolicyDraft pd GROUP BY pd.status") + List getDraftStatisticsByStatus(); + + /** + * Get drafts summary by user. + */ + @Query("SELECT pd.createdByUserId, pd.status, COUNT(pd) FROM PolicyDraft pd GROUP BY pd.createdByUserId, pd.status") + List getDraftStatisticsByUser(); + + /** + * Find overdue drafts (target implementation date passed and still not published). + */ + @Query("SELECT pd FROM PolicyDraft pd WHERE pd.targetImplementationDate < :currentDate AND pd.status != 'PUBLISHED' AND pd.status != 'REJECTED' AND pd.status != 'ARCHIVED'") + List findOverdueDrafts(@Param("currentDate") Instant currentDate); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/repository/PolicyEvaluationRepository.java b/src/main/java/com/dalab/policyengine/repository/PolicyEvaluationRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..f1b0efe382db41ec443e868965b21620c4f5ebdc --- /dev/null +++ b/src/main/java/com/dalab/policyengine/repository/PolicyEvaluationRepository.java @@ -0,0 +1,20 @@ +package com.dalab.policyengine.repository; + +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import com.dalab.policyengine.model.PolicyEvaluation; +import com.dalab.policyengine.model.PolicyEvaluationStatus; + +@Repository +public interface PolicyEvaluationRepository extends JpaRepository, JpaSpecificationExecutor { + Page findByPolicyId(UUID policyId, Pageable pageable); + Page findByTargetAssetId(String targetAssetId, Pageable pageable); + Page findByStatus(PolicyEvaluationStatus status, Pageable pageable); + Page findByPolicyIdAndTargetAssetId(UUID policyId, String targetAssetId, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/repository/PolicyRepository.java b/src/main/java/com/dalab/policyengine/repository/PolicyRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..4c4877ebcdf9cdc39c9b72be1edbeec54bea65c6 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/repository/PolicyRepository.java @@ -0,0 +1,18 @@ +package com.dalab.policyengine.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import com.dalab.policyengine.model.Policy; +import com.dalab.policyengine.model.PolicyStatus; + +@Repository +public interface PolicyRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findByName(String name); + List findByStatus(PolicyStatus status); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/repository/PolicyRuleRepository.java b/src/main/java/com/dalab/policyengine/repository/PolicyRuleRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..2b2d6046e67b16754dc86303b98b9d72fbed8151 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/repository/PolicyRuleRepository.java @@ -0,0 +1,14 @@ +package com.dalab.policyengine.repository; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.dalab.policyengine.model.PolicyRule; + +@Repository +public interface PolicyRuleRepository extends JpaRepository { + List findByPolicyId(UUID policyId); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/service/EventSubscriptionService.java b/src/main/java/com/dalab/policyengine/service/EventSubscriptionService.java new file mode 100644 index 0000000000000000000000000000000000000000..f837e5d4b38bfef3c17b6cc93934d648db0235dd --- /dev/null +++ b/src/main/java/com/dalab/policyengine/service/EventSubscriptionService.java @@ -0,0 +1,627 @@ +package com.dalab.policyengine.service; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.Rule; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.api.RulesEngine; +import org.jeasy.rules.core.DefaultRulesEngine; +import org.jeasy.rules.mvel.MVELRule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import com.dalab.policyengine.common.ResourceNotFoundException; +import com.dalab.policyengine.dto.EventAnalyticsDTO; +import com.dalab.policyengine.dto.EventStreamDTO; +import com.dalab.policyengine.dto.EventSubscriptionInputDTO; +import com.dalab.policyengine.dto.EventSubscriptionOutputDTO; +import com.dalab.policyengine.model.EventRule; +import com.dalab.policyengine.model.EventSubscription; +import com.dalab.policyengine.model.EventSubscriptionStatus; +import com.dalab.policyengine.model.EventType; +import com.dalab.policyengine.repository.EventRuleRepository; +import com.dalab.policyengine.repository.EventSubscriptionRepository; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.persistence.criteria.Predicate; + +/** + * Service implementation for Event Subscription management and event streaming. + * Provides operations for creating, managing, and processing event subscriptions. + */ +@Service +@Transactional +public class EventSubscriptionService implements IEventSubscriptionService { + + private static final Logger log = LoggerFactory.getLogger(EventSubscriptionService.class); + + private final EventSubscriptionRepository eventSubscriptionRepository; + private final EventRuleRepository eventRuleRepository; + private final ObjectMapper objectMapper; + private final RulesEngine rulesEngine; + + @Autowired + public EventSubscriptionService(EventSubscriptionRepository eventSubscriptionRepository, + EventRuleRepository eventRuleRepository, + ObjectMapper objectMapper) { + this.eventSubscriptionRepository = eventSubscriptionRepository; + this.eventRuleRepository = eventRuleRepository; + this.objectMapper = objectMapper; + this.rulesEngine = new DefaultRulesEngine(); + } + + // ==================== SUBSCRIPTION MANAGEMENT ==================== + + @Override + public EventSubscriptionOutputDTO createSubscription(EventSubscriptionInputDTO inputDTO, UUID creatorUserId) { + log.info("Creating event subscription for user: {}", creatorUserId); + + // Validate input + validateSubscriptionConfiguration(inputDTO); + + // Create subscription entity + EventSubscription subscription = new EventSubscription(); + subscription.setName(inputDTO.getName()); + subscription.setDescription(inputDTO.getDescription()); + subscription.setUserId(creatorUserId); + subscription.setStatus(EventSubscriptionStatus.ACTIVE); + subscription.setNotificationChannels(inputDTO.getNotificationChannels()); + subscription.setNotificationSettings(inputDTO.getNotificationSettings()); + subscription.setCreatedAt(Instant.now()); + subscription.setUpdatedAt(Instant.now()); + subscription.setCreatedByUserId(creatorUserId); + subscription.setUpdatedByUserId(creatorUserId); + + // Save subscription + EventSubscription savedSubscription = eventSubscriptionRepository.save(subscription); + + // Create and save rules + if (inputDTO.getRules() != null && !inputDTO.getRules().isEmpty()) { + List rules = inputDTO.getRules().stream() + .map(ruleInput -> { + EventRule rule = new EventRule(); + rule.setSubscriptionId(savedSubscription.getId()); + rule.setCondition(ruleInput.getCondition()); + rule.setEventTypes(ruleInput.getEventTypes()); + rule.setSeverityLevels(ruleInput.getSeverityLevels()); + rule.setSourceServices(ruleInput.getSourceServices()); + rule.setCreatedAt(Instant.now()); + return eventRuleRepository.save(rule); + }) + .collect(Collectors.toList()); + + savedSubscription.setRules(rules); + } + + log.info("Successfully created event subscription: {}", savedSubscription.getId()); + return convertToOutputDTO(savedSubscription); + } + + @Override + public EventSubscriptionOutputDTO updateSubscription(UUID subscriptionId, EventSubscriptionInputDTO inputDTO, UUID updaterUserId) { + log.info("Updating event subscription: {}", subscriptionId); + + EventSubscription subscription = eventSubscriptionRepository.findById(subscriptionId) + .orElseThrow(() -> new ResourceNotFoundException("EventSubscription", "id", subscriptionId.toString())); + + // Validate input + validateSubscriptionConfiguration(inputDTO); + + // Update subscription fields + subscription.setName(inputDTO.getName()); + subscription.setDescription(inputDTO.getDescription()); + subscription.setNotificationChannels(inputDTO.getNotificationChannels()); + subscription.setNotificationSettings(inputDTO.getNotificationSettings()); + subscription.setUpdatedAt(Instant.now()); + subscription.setUpdatedByUserId(updaterUserId); + + // Update rules + if (inputDTO.getRules() != null) { + // Delete existing rules + eventRuleRepository.deleteBySubscriptionId(subscriptionId); + + // Create new rules + List rules = inputDTO.getRules().stream() + .map(ruleInput -> { + EventRule rule = new EventRule(); + rule.setSubscriptionId(subscriptionId); + rule.setCondition(ruleInput.getCondition()); + rule.setEventTypes(ruleInput.getEventTypes()); + rule.setSeverityLevels(ruleInput.getSeverityLevels()); + rule.setSourceServices(ruleInput.getSourceServices()); + rule.setCreatedAt(Instant.now()); + return eventRuleRepository.save(rule); + }) + .collect(Collectors.toList()); + + subscription.setRules(rules); + } + + EventSubscription updatedSubscription = eventSubscriptionRepository.save(subscription); + log.info("Successfully updated event subscription: {}", subscriptionId); + return convertToOutputDTO(updatedSubscription); + } + + @Override + @Transactional(readOnly = true) + public EventSubscriptionOutputDTO getSubscriptionById(UUID subscriptionId) { + EventSubscription subscription = eventSubscriptionRepository.findById(subscriptionId) + .orElseThrow(() -> new ResourceNotFoundException("EventSubscription", "id", subscriptionId.toString())); + return convertToOutputDTO(subscription); + } + + @Override + @Transactional(readOnly = true) + public Page getSubscriptionsForUser(UUID userId, Pageable pageable, String status, String nameContains) { + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + predicates.add(criteriaBuilder.equal(root.get("userId"), userId)); + + if (StringUtils.hasText(status)) { + try { + predicates.add(criteriaBuilder.equal(root.get("status"), EventSubscriptionStatus.valueOf(status.toUpperCase()))); + } catch (IllegalArgumentException e) { + log.warn("Invalid status provided for filtering: {}", status); + predicates.add(criteriaBuilder.disjunction()); + } + } + + if (StringUtils.hasText(nameContains)) { + predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), + "%" + nameContains.toLowerCase() + "%")); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + Page subscriptionPage = eventSubscriptionRepository.findAll(spec, pageable); + return subscriptionPage.map(this::convertToOutputDTO); + } + + @Override + @Transactional(readOnly = true) + public Page getAllSubscriptions(Pageable pageable, String status, String nameContains) { + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + if (StringUtils.hasText(status)) { + try { + predicates.add(criteriaBuilder.equal(root.get("status"), EventSubscriptionStatus.valueOf(status.toUpperCase()))); + } catch (IllegalArgumentException e) { + log.warn("Invalid status provided for filtering: {}", status); + predicates.add(criteriaBuilder.disjunction()); + } + } + + if (StringUtils.hasText(nameContains)) { + predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), + "%" + nameContains.toLowerCase() + "%")); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + Page subscriptionPage = eventSubscriptionRepository.findAll(spec, pageable); + return subscriptionPage.map(this::convertToOutputDTO); + } + + @Override + public void deleteSubscription(UUID subscriptionId) { + if (!eventSubscriptionRepository.existsById(subscriptionId)) { + throw new ResourceNotFoundException("EventSubscription", "id", subscriptionId.toString()); + } + + // Delete associated rules first + eventRuleRepository.deleteBySubscriptionId(subscriptionId); + + // Delete subscription + eventSubscriptionRepository.deleteById(subscriptionId); + log.info("Successfully deleted event subscription: {}", subscriptionId); + } + + @Override + public EventSubscriptionOutputDTO updateSubscriptionStatus(UUID subscriptionId, EventSubscriptionStatus status, UUID updaterUserId) { + EventSubscription subscription = eventSubscriptionRepository.findById(subscriptionId) + .orElseThrow(() -> new ResourceNotFoundException("EventSubscription", "id", subscriptionId.toString())); + + subscription.setStatus(status); + subscription.setUpdatedAt(Instant.now()); + subscription.setUpdatedByUserId(updaterUserId); + + EventSubscription updatedSubscription = eventSubscriptionRepository.save(subscription); + log.info("Updated subscription {} status to: {}", subscriptionId, status); + return convertToOutputDTO(updatedSubscription); + } + + // ==================== EVENT STREAMING AND PROCESSING ==================== + + @Override + @Transactional(readOnly = true) + public List getEventStreamForUser(UUID userId, Integer limit) { + log.info("Getting event stream for user: {} with limit: {}", userId, limit); + + // Get user's active subscriptions + List activeSubscriptions = eventSubscriptionRepository + .findByUserIdAndStatus(userId, EventSubscriptionStatus.ACTIVE); + + if (activeSubscriptions.isEmpty()) { + return new ArrayList<>(); + } + + // For now, return mock events that would match user's subscriptions + // In a real implementation, this would query actual event streams + return generateMockEventsForSubscriptions(activeSubscriptions, limit != null ? limit : 50); + } + + @Override + @Transactional(readOnly = true) + public List getAllEventStream(Integer limit) { + log.info("Getting all event stream with limit: {}", limit); + + // Return mock events for all subscriptions + List allSubscriptions = eventSubscriptionRepository.findAll(); + return generateMockEventsForSubscriptions(allSubscriptions, limit != null ? limit : 100); + } + + @Override + public void processIncomingEvent(EventStreamDTO eventDTO) { + log.info("Processing incoming event: {} from service: {}", eventDTO.getEventType(), eventDTO.getSourceService()); + + // Find all active subscriptions that might match this event + List activeSubscriptions = eventSubscriptionRepository + .findByStatus(EventSubscriptionStatus.ACTIVE); + + for (EventSubscription subscription : activeSubscriptions) { + try { + if (matchesSubscription(eventDTO, subscription)) { + log.info("Event matches subscription: {}", subscription.getId()); + // In a real implementation, this would trigger notifications + // For now, just log the match + } + } catch (Exception e) { + log.error("Error processing event for subscription {}: {}", subscription.getId(), e.getMessage(), e); + } + } + } + + @Override + @Transactional(readOnly = true) + public Page getHistoricalEventsForSubscription(UUID subscriptionId, Pageable pageable) { + // Verify subscription exists + if (!eventSubscriptionRepository.existsById(subscriptionId)) { + throw new ResourceNotFoundException("EventSubscription", "id", subscriptionId.toString()); + } + + // For now, return mock historical events + // In a real implementation, this would query actual event history + List mockEvents = generateMockHistoricalEvents(subscriptionId, pageable.getPageSize()); + + // Create a simple page implementation + return new org.springframework.data.domain.PageImpl<>( + mockEvents, + pageable, + mockEvents.size() + ); + } + + // ==================== ANALYTICS AND METRICS ==================== + + @Override + @Transactional(readOnly = true) + public EventAnalyticsDTO getEventAnalyticsForUser(UUID userId) { + log.info("Getting event analytics for user: {}", userId); + + List userSubscriptions = eventSubscriptionRepository.findByUserId(userId); + + EventAnalyticsDTO analytics = new EventAnalyticsDTO(); + analytics.setUserId(userId); + analytics.setTotalSubscriptions(userSubscriptions.size()); + analytics.setActiveSubscriptions((int) userSubscriptions.stream() + .filter(s -> s.getStatus() == EventSubscriptionStatus.ACTIVE) + .count()); + + // Add mock metrics + EventAnalyticsDTO.EventMetricsDTO metrics = new EventAnalyticsDTO.EventMetricsDTO(); + metrics.setTotalEvents(1250); + metrics.setEventsToday(45); + metrics.setEventsThisWeek(320); + metrics.setEventsThisMonth(1250); + analytics.setEventMetrics(metrics); + + // Add mock trends + EventAnalyticsDTO.EventTrendsDTO trends = new EventAnalyticsDTO.EventTrendsDTO(); + trends.setTrendDirection("increasing"); + trends.setTrendPercentage(12.5); + trends.setPeakHour("14:00"); + trends.setPeakDay("Wednesday"); + analytics.setEventTrends(trends); + + return analytics; + } + + @Override + @Transactional(readOnly = true) + public EventAnalyticsDTO getSystemEventAnalytics() { + log.info("Getting system-wide event analytics"); + + List allSubscriptions = eventSubscriptionRepository.findAll(); + + EventAnalyticsDTO analytics = new EventAnalyticsDTO(); + analytics.setTotalSubscriptions(allSubscriptions.size()); + analytics.setActiveSubscriptions((int) allSubscriptions.stream() + .filter(s -> s.getStatus() == EventSubscriptionStatus.ACTIVE) + .count()); + + // Add comprehensive system metrics + EventAnalyticsDTO.EventMetricsDTO metrics = new EventAnalyticsDTO.EventMetricsDTO(); + metrics.setTotalEvents(8750); + metrics.setEventsToday(245); + metrics.setEventsThisWeek(1820); + metrics.setEventsThisMonth(8750); + analytics.setEventMetrics(metrics); + + // Add system trends + EventAnalyticsDTO.EventTrendsDTO trends = new EventAnalyticsDTO.EventTrendsDTO(); + trends.setTrendDirection("stable"); + trends.setTrendPercentage(2.1); + trends.setPeakHour("10:00"); + trends.setPeakDay("Tuesday"); + analytics.setEventTrends(trends); + + return analytics; + } + + @Override + @Transactional(readOnly = true) + public EventAnalyticsDTO getEventAnalyticsForTimeRange(UUID userId, Instant fromTime, Instant toTime) { + log.info("Getting event analytics for user: {} from {} to {}", userId, fromTime, toTime); + + // For now, return mock analytics for the time range + // In a real implementation, this would query actual event data + EventAnalyticsDTO analytics = new EventAnalyticsDTO(); + analytics.setUserId(userId); + analytics.setTotalSubscriptions(5); + analytics.setActiveSubscriptions(3); + + EventAnalyticsDTO.EventMetricsDTO metrics = new EventAnalyticsDTO.EventMetricsDTO(); + metrics.setTotalEvents(450); + metrics.setEventsToday(25); + metrics.setEventsThisWeek(180); + metrics.setEventsThisMonth(450); + analytics.setEventMetrics(metrics); + + return analytics; + } + + // ==================== UTILITY METHODS ==================== + + @Override + public boolean testEventRule(String ruleCondition, EventStreamDTO sampleEvent) { + try { + // Create MVEL rule for testing + Rule testRule = new MVELRule() + .name("Test Rule") + .description("Test rule for validation") + .when(ruleCondition) + .then("true"); + + Rules rules = new Rules(testRule); + Facts facts = new Facts(); + + // Add event data to facts + facts.put("event", sampleEvent); + facts.put("eventType", sampleEvent.getEventType()); + facts.put("severity", sampleEvent.getSeverity()); + facts.put("sourceService", sampleEvent.getSourceService()); + facts.put("assetId", sampleEvent.getAssetId()); + facts.put("timestamp", sampleEvent.getTimestamp()); + + // Execute rule + rulesEngine.fire(rules, facts); + + // Check if rule was triggered + return facts.get("true") != null; + + } catch (Exception e) { + log.error("Error testing event rule: {}", e.getMessage(), e); + return false; + } + } + + @Override + public List getAvailableEventTypes() { + return Arrays.asList(EventType.values()); + } + + @Override + public List getAvailableSourceServices() { + return Arrays.asList( + "da-discovery", + "da-catalog", + "da-policyengine", + "da-reporting", + "da-autolabel", + "da-autoarchival", + "da-autodelete", + "da-autocompliance", + "da-admin-service" + ); + } + + @Override + public void validateSubscriptionConfiguration(EventSubscriptionInputDTO inputDTO) { + if (inputDTO == null) { + throw new IllegalArgumentException("Subscription input cannot be null"); + } + + if (!StringUtils.hasText(inputDTO.getName())) { + throw new IllegalArgumentException("Subscription name is required"); + } + + if (inputDTO.getName().length() > 255) { + throw new IllegalArgumentException("Subscription name cannot exceed 255 characters"); + } + + if (inputDTO.getNotificationChannels() == null || inputDTO.getNotificationChannels().isEmpty()) { + throw new IllegalArgumentException("At least one notification channel is required"); + } + + // Validate rules if present + if (inputDTO.getRules() != null) { + for (EventSubscriptionInputDTO.RuleInputDTO rule : inputDTO.getRules()) { + if (StringUtils.hasText(rule.getCondition())) { + // Test the MVEL condition syntax + try { + new MVELRule() + .name("Validation Rule") + .description("Rule for validation") + .when(rule.getCondition()) + .then("true"); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid rule condition: " + e.getMessage()); + } + } + } + } + } + + // ==================== PRIVATE HELPER METHODS ==================== + + private EventSubscriptionOutputDTO convertToOutputDTO(EventSubscription subscription) { + EventSubscriptionOutputDTO outputDTO = new EventSubscriptionOutputDTO(); + outputDTO.setId(subscription.getId()); + outputDTO.setName(subscription.getName()); + outputDTO.setDescription(subscription.getDescription()); + outputDTO.setUserId(subscription.getUserId()); + outputDTO.setStatus(subscription.getStatus()); + outputDTO.setNotificationChannels(subscription.getNotificationChannels()); + outputDTO.setNotificationSettings(subscription.getNotificationSettings()); + outputDTO.setCreatedAt(subscription.getCreatedAt()); + outputDTO.setUpdatedAt(subscription.getUpdatedAt()); + outputDTO.setCreatedByUserId(subscription.getCreatedByUserId()); + outputDTO.setUpdatedByUserId(subscription.getUpdatedByUserId()); + + // Convert rules + if (subscription.getRules() != null) { + List ruleDTOs = subscription.getRules().stream() + .map(this::convertRuleToOutputDTO) + .collect(Collectors.toList()); + outputDTO.setRules(ruleDTOs); + } + + // Add mock statistics + EventSubscriptionOutputDTO.SubscriptionStatsDTO stats = new EventSubscriptionOutputDTO.SubscriptionStatsDTO(); + stats.setTotalEvents(125); + stats.setEventsToday(5); + stats.setLastEventAt(Instant.now().minusSeconds(3600)); + outputDTO.setStatistics(stats); + + return outputDTO; + } + + private EventSubscriptionOutputDTO.RuleOutputDTO convertRuleToOutputDTO(EventRule rule) { + EventSubscriptionOutputDTO.RuleOutputDTO ruleDTO = new EventSubscriptionOutputDTO.RuleOutputDTO(); + ruleDTO.setId(rule.getId()); + ruleDTO.setCondition(rule.getCondition()); + ruleDTO.setEventTypes(rule.getEventTypes()); + ruleDTO.setSeverityLevels(rule.getSeverityLevels()); + ruleDTO.setSourceServices(rule.getSourceServices()); + ruleDTO.setCreatedAt(rule.getCreatedAt()); + return ruleDTO; + } + + private boolean matchesSubscription(EventStreamDTO event, EventSubscription subscription) { + if (subscription.getRules() == null || subscription.getRules().isEmpty()) { + return true; // No rules means match all events + } + + for (EventRule rule : subscription.getRules()) { + try { + // Create MVEL rule for matching + Rule mvelRule = new MVELRule() + .name("Subscription Rule") + .description("Rule for subscription matching") + .when(rule.getCondition()) + .then("true"); + + Rules rules = new Rules(mvelRule); + Facts facts = new Facts(); + + // Add event data to facts + facts.put("event", event); + facts.put("eventType", event.getEventType()); + facts.put("severity", event.getSeverity()); + facts.put("sourceService", event.getSourceService()); + facts.put("assetId", event.getAssetId()); + facts.put("timestamp", event.getTimestamp()); + + // Execute rule + rulesEngine.fire(rules, facts); + + // If rule was triggered, event matches + if (facts.get("true") != null) { + return true; + } + + } catch (Exception e) { + log.error("Error matching event against rule {}: {}", rule.getId(), e.getMessage(), e); + } + } + + return false; + } + + private List generateMockEventsForSubscriptions(List subscriptions, int limit) { + List events = new ArrayList<>(); + + for (int i = 0; i < Math.min(limit, subscriptions.size() * 3); i++) { + EventStreamDTO event = new EventStreamDTO(); + event.setId(UUID.randomUUID()); + event.setEventType(EventType.ASSET_DISCOVERED); + event.setSeverity("INFO"); + event.setSourceService("da-discovery"); + event.setAssetId("asset-" + (i + 1)); + event.setTimestamp(Instant.now().minusSeconds(i * 60)); + event.setMessage("Mock event for testing"); + event.setMetadata(Map.of("test", "true", "index", String.valueOf(i))); + + events.add(event); + } + + return events; + } + + private List generateMockHistoricalEvents(UUID subscriptionId, int limit) { + List events = new ArrayList<>(); + + for (int i = 0; i < limit; i++) { + EventStreamDTO event = new EventStreamDTO(); + event.setId(UUID.randomUUID()); + event.setEventType(EventType.POLICY_VIOLATION); + event.setSeverity("WARNING"); + event.setSourceService("da-policyengine"); + event.setAssetId("asset-" + (i + 1)); + event.setTimestamp(Instant.now().minusSeconds(i * 3600)); // Historical events + event.setMessage("Historical mock event"); + event.setMetadata(Map.of("subscriptionId", subscriptionId.toString(), "historical", "true")); + + events.add(event); + } + + return events; + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/service/IEventSubscriptionService.java b/src/main/java/com/dalab/policyengine/service/IEventSubscriptionService.java new file mode 100644 index 0000000000000000000000000000000000000000..65f28d3423c0c44f62ba749726aaa3efcecfd8e5 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/service/IEventSubscriptionService.java @@ -0,0 +1,120 @@ +package com.dalab.policyengine.service; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.dalab.policyengine.dto.EventAnalyticsDTO; +import com.dalab.policyengine.dto.EventStreamDTO; +import com.dalab.policyengine.dto.EventSubscriptionInputDTO; +import com.dalab.policyengine.dto.EventSubscriptionOutputDTO; +import com.dalab.policyengine.model.EventSubscriptionStatus; +import com.dalab.policyengine.model.EventType; + +/** + * Service interface for Event Subscription management and event streaming. + * Provides operations for creating, managing, and processing event subscriptions. + */ +public interface IEventSubscriptionService { + + // Subscription Management + + /** + * Create a new event subscription for the authenticated user + */ + EventSubscriptionOutputDTO createSubscription(EventSubscriptionInputDTO inputDTO, UUID creatorUserId); + + /** + * Update an existing event subscription + */ + EventSubscriptionOutputDTO updateSubscription(UUID subscriptionId, EventSubscriptionInputDTO inputDTO, UUID updaterUserId); + + /** + * Get event subscription by ID + */ + EventSubscriptionOutputDTO getSubscriptionById(UUID subscriptionId); + + /** + * Get all event subscriptions for a user with pagination + */ + Page getSubscriptionsForUser(UUID userId, Pageable pageable, String status, String nameContains); + + /** + * Get all event subscriptions (admin only) with pagination + */ + Page getAllSubscriptions(Pageable pageable, String status, String nameContains); + + /** + * Delete an event subscription + */ + void deleteSubscription(UUID subscriptionId); + + /** + * Update subscription status (enable/disable/pause) + */ + EventSubscriptionOutputDTO updateSubscriptionStatus(UUID subscriptionId, EventSubscriptionStatus status, UUID updaterUserId); + + // Event Streaming and Processing + + /** + * Get real-time event stream for a user's subscriptions + * This would typically be used with WebSocket or Server-Sent Events + */ + List getEventStreamForUser(UUID userId, Integer limit); + + /** + * Get event stream for all subscriptions (admin only) + */ + List getAllEventStream(Integer limit); + + /** + * Process an incoming event and match it against active subscriptions + */ + void processIncomingEvent(EventStreamDTO eventDTO); + + /** + * Get historical events for a subscription + */ + Page getHistoricalEventsForSubscription(UUID subscriptionId, Pageable pageable); + + // Analytics and Metrics + + /** + * Get event analytics for a user's subscriptions + */ + EventAnalyticsDTO getEventAnalyticsForUser(UUID userId); + + /** + * Get system-wide event analytics (admin only) + */ + EventAnalyticsDTO getSystemEventAnalytics(); + + /** + * Get event analytics for a specific time range + */ + EventAnalyticsDTO getEventAnalyticsForTimeRange(UUID userId, java.time.Instant fromTime, java.time.Instant toTime); + + // Utility Methods + + /** + * Test event rules against sample data + */ + boolean testEventRule(String ruleCondition, EventStreamDTO sampleEvent); + + /** + * Get available event types for subscription configuration + */ + List getAvailableEventTypes(); + + /** + * Get available source services for subscription configuration + */ + List getAvailableSourceServices(); + + /** + * Validate subscription configuration + */ + void validateSubscriptionConfiguration(EventSubscriptionInputDTO inputDTO); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/service/IPolicyEvaluationService.java b/src/main/java/com/dalab/policyengine/service/IPolicyEvaluationService.java new file mode 100644 index 0000000000000000000000000000000000000000..3a8a4189db5d5cdb141610cb54a66d07ad0495f5 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/service/IPolicyEvaluationService.java @@ -0,0 +1,26 @@ +package com.dalab.policyengine.service; + +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.dalab.common.event.AssetChangeEvent; +import com.dalab.policyengine.dto.PolicyEvaluationOutputDTO; +import com.dalab.policyengine.dto.PolicyEvaluationRequestDTO; +import com.dalab.policyengine.dto.PolicyEvaluationSummaryDTO; + +public interface IPolicyEvaluationService { + + PolicyEvaluationOutputDTO evaluatePolicyForAsset(UUID policyId, PolicyEvaluationRequestDTO evaluationRequest, UUID triggeredByUserId); + + // Method expected by controller - delegates to evaluatePolicyForAsset + PolicyEvaluationOutputDTO triggerPolicyEvaluation(UUID policyId, PolicyEvaluationRequestDTO evaluationRequest, UUID triggeredByUserId); + + Page getPolicyEvaluations(Pageable pageable, UUID policyId, String targetAssetId, String status); + + PolicyEvaluationOutputDTO getPolicyEvaluationById(UUID evaluationId); + + // Internal method for Kafka consumer or scheduled tasks + void evaluatePolicyForAssetInternal(AssetChangeEvent assetChangeEvent, UUID eventInitiatorId); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/service/IPolicyService.java b/src/main/java/com/dalab/policyengine/service/IPolicyService.java new file mode 100644 index 0000000000000000000000000000000000000000..fc6b9d68c575e854acd1b77948dca088afa06492 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/service/IPolicyService.java @@ -0,0 +1,225 @@ +package com.dalab.policyengine.service; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.dalab.policyengine.dto.PolicyDraftActionDTO; +import com.dalab.policyengine.dto.PolicyDraftInputDTO; +import com.dalab.policyengine.dto.PolicyDraftOutputDTO; +import com.dalab.policyengine.dto.PolicyImpactRequestDTO; +import com.dalab.policyengine.dto.PolicyImpactResponseDTO; +import com.dalab.policyengine.dto.PolicyInputDTO; +import com.dalab.policyengine.dto.PolicyOutputDTO; +import com.dalab.policyengine.dto.PolicySummaryDTO; +import com.dalab.policyengine.model.PolicyDraftStatus; + +public interface IPolicyService { + Page getAllPolicies(Pageable pageable, String status, String nameContains); + PolicyOutputDTO getPolicyById(UUID policyId); + PolicyOutputDTO createPolicy(PolicyInputDTO policyInputDTO, UUID creatorUserId); + PolicyOutputDTO updatePolicy(UUID policyId, PolicyInputDTO policyInputDTO, UUID updaterUserId); + void deletePolicy(UUID policyId); + + /** + * Analyze the potential impact of a policy before implementation. + * Provides comprehensive assessment of affected assets, performance impact, + * cost implications, and compliance analysis. + * + * @param request the policy impact analysis request containing rules and analysis parameters + * @return comprehensive impact analysis response with affected assets and risk assessment + */ + PolicyImpactResponseDTO analyzePolicy(PolicyImpactRequestDTO request); + + /** + * Create a new policy draft. + * + * @param draftInput the draft input data + * @param creatorUserId the ID of the user creating the draft + * @return the created draft output DTO + */ + PolicyDraftOutputDTO createDraft(PolicyDraftInputDTO draftInput, UUID creatorUserId); + + /** + * Update an existing policy draft (only allowed in CREATED or REQUIRES_CHANGES status). + * + * @param draftId the ID of the draft to update + * @param draftInput the updated draft data + * @param updaterUserId the ID of the user updating the draft + * @return the updated draft output DTO + */ + PolicyDraftOutputDTO updateDraft(UUID draftId, PolicyDraftInputDTO draftInput, UUID updaterUserId); + + /** + * Get a policy draft by ID with full details including workflow information. + * + * @param draftId the ID of the draft + * @return the draft output DTO with complete information + */ + PolicyDraftOutputDTO getDraftById(UUID draftId); + + /** + * Get all policy drafts with filtering and pagination. + * + * @param pageable pagination information + * @param status optional status filter + * @param category optional category filter + * @param priority optional priority filter + * @param createdBy optional creator filter + * @param nameContains optional name search filter + * @return paginated list of draft summaries + */ + Page getAllDrafts( + Pageable pageable, + PolicyDraftStatus status, + String category, + String priority, + UUID createdBy, + String nameContains + ); + + /** + * Get drafts requiring attention from a specific user (created by them or they are stakeholder). + * + * @param userId the user ID + * @param pageable pagination information + * @return paginated list of drafts requiring attention + */ + Page getDraftsRequiringAttention(UUID userId, Pageable pageable); + + /** + * Get drafts pending review (submitted or under review status). + * + * @param pageable pagination information + * @return paginated list of drafts pending review + */ + Page getDraftsPendingReview(Pageable pageable); + + /** + * Submit a draft for review (transition from CREATED to SUBMITTED). + * + * @param draftId the ID of the draft to submit + * @param action the submit action DTO with optional comment + * @param submittedByUserId the ID of the user submitting the draft + * @return the updated draft output DTO + */ + PolicyDraftOutputDTO submitDraft(UUID draftId, PolicyDraftActionDTO action, UUID submittedByUserId); + + /** + * Approve a draft (transition to APPROVED status). + * + * @param draftId the ID of the draft to approve + * @param action the approval action DTO with optional comment + * @param approverUserId the ID of the user approving the draft + * @return the updated draft output DTO + */ + PolicyDraftOutputDTO approveDraft(UUID draftId, PolicyDraftActionDTO action, UUID approverUserId); + + /** + * Reject a draft (transition to REJECTED status). + * + * @param draftId the ID of the draft to reject + * @param action the rejection action DTO with required comment + * @param rejectorUserId the ID of the user rejecting the draft + * @return the updated draft output DTO + */ + PolicyDraftOutputDTO rejectDraft(UUID draftId, PolicyDraftActionDTO action, UUID rejectorUserId); + + /** + * Request changes to a draft (transition to REQUIRES_CHANGES status). + * + * @param draftId the ID of the draft requiring changes + * @param action the action DTO with required comment explaining needed changes + * @param reviewerUserId the ID of the user requesting changes + * @return the updated draft output DTO + */ + PolicyDraftOutputDTO requestChanges(UUID draftId, PolicyDraftActionDTO action, UUID reviewerUserId); + + /** + * Publish an approved draft as an active policy. + * + * @param draftId the ID of the draft to publish + * @param action the publish action DTO with optional metadata + * @param publisherUserId the ID of the user publishing the draft + * @return the published policy output DTO + */ + PolicyOutputDTO publishDraft(UUID draftId, PolicyDraftActionDTO action, UUID publisherUserId); + + /** + * Archive a draft (transition to ARCHIVED status). + * + * @param draftId the ID of the draft to archive + * @param action the archive action DTO with optional comment + * @param archiverUserId the ID of the user archiving the draft + * @return the updated draft output DTO + */ + PolicyDraftOutputDTO archiveDraft(UUID draftId, PolicyDraftActionDTO action, UUID archiverUserId); + + /** + * Add a review comment to a draft. + * + * @param draftId the ID of the draft + * @param comment the review comment + * @param reviewerUserId the ID of the reviewer + * @param reviewerRole the role of the reviewer + * @return the updated draft output DTO + */ + PolicyDraftOutputDTO addReviewComment(UUID draftId, String comment, UUID reviewerUserId, String reviewerRole); + + /** + * Delete a draft (only allowed in CREATED status). + * + * @param draftId the ID of the draft to delete + * @param deleterUserId the ID of the user deleting the draft + */ + void deleteDraft(UUID draftId, UUID deleterUserId); + + /** + * Get available categories for drafts. + * + * @return list of available categories + */ + List getDraftCategories(); + + /** + * Get available tags used in drafts. + * + * @return list of available tags + */ + List getDraftTags(); + + /** + * Get draft statistics for dashboard displays. + * + * @return draft statistics grouped by status + */ + List getDraftStatistics(); + + /** + * Get overdue drafts (target implementation date passed and not published). + * + * @return list of overdue drafts + */ + List getOverdueDrafts(); + + /** + * Clone an existing policy as a new draft for modification. + * + * @param policyId the ID of the policy to clone + * @param creatorUserId the ID of the user creating the draft + * @return the created draft output DTO + */ + PolicyDraftOutputDTO clonePolicyAsDraft(UUID policyId, UUID creatorUserId); + + /** + * Create a new version of an existing draft. + * + * @param draftId the ID of the draft to create new version from + * @param draftInput the updated draft data + * @param creatorUserId the ID of the user creating the new version + * @return the new version draft output DTO + */ + PolicyDraftOutputDTO createDraftVersion(UUID draftId, PolicyDraftInputDTO draftInput, UUID creatorUserId); +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/service/PolicyEvaluationService.java b/src/main/java/com/dalab/policyengine/service/PolicyEvaluationService.java new file mode 100644 index 0000000000000000000000000000000000000000..9daf5fad2ff3b6f68af31d9acc1b48d4c102506f --- /dev/null +++ b/src/main/java/com/dalab/policyengine/service/PolicyEvaluationService.java @@ -0,0 +1,295 @@ +package com.dalab.policyengine.service; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.Rule; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.api.RulesEngine; +import org.jeasy.rules.core.DefaultRulesEngine; +import org.jeasy.rules.mvel.MVELRule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import com.dalab.common.event.AssetChangeEvent; +import com.dalab.policyengine.common.ResourceNotFoundException; +import com.dalab.policyengine.dto.PolicyEvaluationOutputDTO; +import com.dalab.policyengine.dto.PolicyEvaluationRequestDTO; +import com.dalab.policyengine.dto.PolicyEvaluationSummaryDTO; +import com.dalab.policyengine.kafka.producer.PolicyActionProducer; +import com.dalab.policyengine.mapper.PolicyMapper; +import com.dalab.policyengine.model.Policy; +import com.dalab.policyengine.model.PolicyEvaluation; +import com.dalab.policyengine.model.PolicyEvaluationStatus; +import com.dalab.policyengine.model.PolicyRule; +import com.dalab.policyengine.model.PolicyStatus; +import com.dalab.policyengine.repository.PolicyEvaluationRepository; +import com.dalab.policyengine.repository.PolicyRepository; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.Timestamp; + +import jakarta.persistence.criteria.Predicate; + +@Service +@Transactional +public class PolicyEvaluationService implements IPolicyEvaluationService { + + private static final Logger log = LoggerFactory.getLogger(PolicyEvaluationService.class); + + private final PolicyRepository policyRepository; + private final PolicyEvaluationRepository policyEvaluationRepository; + private final PolicyMapper policyMapper; + private final RulesEngine rulesEngine; + private final ObjectMapper objectMapper; // For converting asset data and actions + private final PolicyActionProducer policyActionProducer; + + @Autowired + public PolicyEvaluationService(PolicyRepository policyRepository, + PolicyEvaluationRepository policyEvaluationRepository, + PolicyMapper policyMapper, + ObjectMapper objectMapper, + PolicyActionProducer policyActionProducer) { + this.policyRepository = policyRepository; + this.policyEvaluationRepository = policyEvaluationRepository; + this.policyMapper = policyMapper; + this.objectMapper = objectMapper; + this.rulesEngine = new DefaultRulesEngine(); + this.policyActionProducer = policyActionProducer; + } + + @Override + public PolicyEvaluationOutputDTO evaluatePolicyForAsset(UUID policyId, PolicyEvaluationRequestDTO evaluationRequest, UUID triggeredByUserId) { + Policy policy = policyRepository.findById(policyId) + .orElseThrow(() -> new ResourceNotFoundException("Policy", "id", policyId.toString())); + + if (policy.getStatus() == PolicyStatus.DISABLED) { + log.warn("Policy {} is disabled. Evaluation skipped for asset {}.", policy.getName(), evaluationRequest.getTargetAssetId()); + PolicyEvaluation evaluation = createEvaluationRecord(policy, evaluationRequest.getTargetAssetId(), PolicyEvaluationStatus.NOT_APPLICABLE, triggeredByUserId, Collections.singletonMap("reason", "Policy disabled"), null); + return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy.getName()); + } + + Map assetData = fetchAssetData(evaluationRequest.getTargetAssetId()); + if (assetData == null || assetData.isEmpty()) { + log.error("Asset data not found for assetId: {}. Evaluation cannot proceed.", evaluationRequest.getTargetAssetId()); + PolicyEvaluation evaluation = createEvaluationRecord(policy, evaluationRequest.getTargetAssetId(), PolicyEvaluationStatus.ERROR, triggeredByUserId, Collections.singletonMap("error", "Asset data not found"), null); + return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy.getName()); + } + + Facts facts = new Facts(); + facts.put("asset", assetData); + if (evaluationRequest.getEvaluationContext() != null) { + evaluationRequest.getEvaluationContext().forEach(facts::put); + } + + Rules easyRules = new Rules(); + Map ruleResults = new HashMap<>(); + for (PolicyRule ruleEntity : policy.getRules()) { + Rule easyRule = new MVELRule() + .name(ruleEntity.getName()) + .description(ruleEntity.getDescription()) + .priority(ruleEntity.getPriority()) + .when(ruleEntity.getCondition()) + .then("ruleResults.put(\"" + ruleEntity.getName() + "\", true); " + + "log.debug(\"Rule '{}' evaluated to true for asset {}\", \"" + ruleEntity.getName() + "\", evaluationRequest.getTargetAssetId());"); + easyRules.register(easyRule); + } + + rulesEngine.fire(easyRules, facts); + + boolean overallPolicyConditionMet = true; + if (StringUtils.hasText(policy.getConditionLogic())) { + overallPolicyConditionMet = ruleResults.values().stream().allMatch(Boolean::booleanValue) && !ruleResults.isEmpty(); + log.debug("Policy condition logic '{}' evaluated to {} based on rule results: {}", policy.getConditionLogic(), overallPolicyConditionMet, ruleResults); + } else { + overallPolicyConditionMet = ruleResults.values().stream().allMatch(Boolean::booleanValue) && !ruleResults.isEmpty(); + log.debug("No policy condition logic. Overall result based on all rules: {}. Rule results: {}", overallPolicyConditionMet, ruleResults); + } + PolicyEvaluationStatus finalStatus = overallPolicyConditionMet ? PolicyEvaluationStatus.PASS : PolicyEvaluationStatus.FAIL; + Map triggeredPolicyActionsSummary = null; + + PolicyEvaluation evaluation = createEvaluationRecord(policy, evaluationRequest.getTargetAssetId(), finalStatus, triggeredByUserId, + Map.of("rulesEvaluated", ruleResults, "factsSnapshot", objectMapper.convertValue(facts.asMap(), new TypeReference>() {})), + null); + + if (finalStatus == PolicyEvaluationStatus.PASS) { + log.info("Policy '{}' PASSED for asset '{}'", policy.getName(), evaluationRequest.getTargetAssetId()); + triggeredPolicyActionsSummary = executePolicyActions(policy, evaluationRequest.getTargetAssetId(), assetData, facts, evaluation.getId()); + evaluation.setTriggeredActions(triggeredPolicyActionsSummary); + policyEvaluationRepository.save(evaluation); + } else { + log.info("Policy '{}' FAILED for asset '{}'", policy.getName(), evaluationRequest.getTargetAssetId()); + } + + return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy.getName()); + } + + @Override + public PolicyEvaluationOutputDTO triggerPolicyEvaluation(UUID policyId, PolicyEvaluationRequestDTO evaluationRequest, UUID triggeredByUserId) { + // Delegate to the main evaluation method + return evaluatePolicyForAsset(policyId, evaluationRequest, triggeredByUserId); + } + + private PolicyEvaluation createEvaluationRecord(Policy policy, String targetAssetId, PolicyEvaluationStatus status, UUID triggeredByUserId, Map details, Map triggeredActions) { + PolicyEvaluation evaluation = new PolicyEvaluation(); + evaluation.setPolicyId(policy.getId()); + evaluation.setTargetAssetId(targetAssetId); + evaluation.setStatus(status); + evaluation.setEvaluationDetails(details); + evaluation.setTriggeredActions(triggeredActions); + evaluation.setEvaluatedAt(Instant.now()); + evaluation.setEvaluationTriggeredByUserId(triggeredByUserId); + return policyEvaluationRepository.save(evaluation); + } + + private Map fetchAssetData(String assetId) { + log.debug("Fetching asset data for assetId: {}", assetId); + Map data = new HashMap<>(); + if ("asset123".equals(assetId)) { + data.put("name", "Sensitive S3 Bucket"); + data.put("assetType", "S3_BUCKET"); + data.put("region", "us-east-1"); + data.put("tags", Arrays.asList("PII", "FinancialData")); + data.put("publicAccess", "true"); + data.put("sizeGB", 1024); + } else if ("asset456".equals(assetId)) { + data.put("name", "Dev EC2 Instance"); + data.put("assetType", "EC2_INSTANCE"); + data.put("region", "eu-west-1"); + data.put("tags", Arrays.asList("Development")); + data.put("instanceType", "t2.micro"); + } + return data.isEmpty() ? null : data; + } + + private Map executePolicyActions(Policy policy, String assetId, Map assetData, Facts facts, UUID evaluationId) { + if (policy.getActions() == null || policy.getActions().isEmpty()) { + return Collections.emptyMap(); + } + log.info("Executing actions for policy '{}' (id={}) on asset '{}': {}", + policy.getName(), policy.getId(), assetId, policy.getActions()); + + Map executedActionsSummary = new HashMap<>(); + Instant now = Instant.now(); + Timestamp eventTimestamp = Timestamp.newBuilder().setSeconds(now.getEpochSecond()).setNanos(now.getNano()).build(); + + final String effectiveAssetId = assetData.getOrDefault("id", assetId).toString(); // Prefer ID from assetData if available + + policy.getActions().forEach((actionType, actionConfig) -> { + // For now, create a simple map-based event instead of protobuf until we add the protobuf definitions + Map policyActionEvent = new HashMap<>(); + policyActionEvent.put("policyId", policy.getId().toString()); + policyActionEvent.put("assetId", effectiveAssetId); + policyActionEvent.put("actionType", actionType); + policyActionEvent.put("timestamp", now.toString()); + + if (evaluationId != null) { + policyActionEvent.put("evaluationId", evaluationId.toString()); + } + + if (actionConfig instanceof Map) { + policyActionEvent.put("actionParameters", actionConfig); + } else if (actionConfig != null) { + policyActionEvent.put("actionParameters", Map.of("value", actionConfig)); + } + + // TODO: Replace with actual protobuf-based PolicyActionEvent when protobuf definitions are added + log.info("Policy action event (simplified): {}", policyActionEvent); + policyActionProducer.sendPolicyActionEvent(policyActionEvent); + + executedActionsSummary.put(actionType, actionConfig); + log.debug("Created PolicyActionEvent for actionType: {} with assetId: {}", actionType, effectiveAssetId); + }); + + return executedActionsSummary; + } + + @Override + @Transactional(readOnly = true) + public Page getPolicyEvaluations(Pageable pageable, UUID policyIdFilter, String targetAssetIdFilter, String statusFilter) { + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + if (policyIdFilter != null) { + predicates.add(criteriaBuilder.equal(root.get("policyId"), policyIdFilter)); + } + if (StringUtils.hasText(targetAssetIdFilter)) { + predicates.add(criteriaBuilder.equal(root.get("targetAssetId"), targetAssetIdFilter)); + } + if (StringUtils.hasText(statusFilter)) { + try { + predicates.add(criteriaBuilder.equal(root.get("status"), PolicyEvaluationStatus.valueOf(statusFilter.toUpperCase()))); + } catch (IllegalArgumentException e) { + log.warn("Invalid policy evaluation status provided for filtering: {}", statusFilter); + predicates.add(criteriaBuilder.disjunction()); + } + } + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + Page evaluationPage = policyEvaluationRepository.findAll(spec, pageable); + + List policyIds = evaluationPage.getContent().stream() + .map(PolicyEvaluation::getPolicyId) + .distinct() + .collect(Collectors.toList()); + Map policyNamesMap = Collections.emptyMap(); + if (!policyIds.isEmpty()) { + policyNamesMap = policyRepository.findAllById(policyIds).stream() + .collect(Collectors.toMap(Policy::getId, Policy::getName)); + } + final Map finalPolicyNamesMap = policyNamesMap; + + return evaluationPage.map(eval -> policyMapper.toPolicyEvaluationSummaryDTO(eval, finalPolicyNamesMap.getOrDefault(eval.getPolicyId(), "Unknown Policy"))); + } + + @Override + @Transactional(readOnly = true) + public PolicyEvaluationOutputDTO getPolicyEvaluationById(UUID evaluationId) { + PolicyEvaluation evaluation = policyEvaluationRepository.findById(evaluationId) + .orElseThrow(() -> new ResourceNotFoundException("PolicyEvaluation", "id", evaluationId.toString())); + Policy policy = policyRepository.findById(evaluation.getPolicyId()) + .orElse(null); // Policy might be deleted, handle gracefully + return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy != null ? policy.getName() : "Unknown/Deleted Policy"); + } + + @Override + public void evaluatePolicyForAssetInternal(AssetChangeEvent assetChangeEvent, UUID eventInitiatorId) { + String assetIdStr = assetChangeEvent.getAssetId(); + log.info("Processing AssetChangeEvent for assetId: {}, eventType: {}", assetIdStr, assetChangeEvent.getEventType()); + + List activePolicies = policyRepository.findByStatus(PolicyStatus.ENABLED); + if (activePolicies.isEmpty()) { + log.info("No active policies found. No evaluations will be triggered for asset {}", assetIdStr); + return; + } + + log.info("Found {} active policies. Triggering evaluations for asset {}...", activePolicies.size(), assetIdStr); + + for (Policy policy : activePolicies) { + try { + PolicyEvaluationRequestDTO requestDTO = new PolicyEvaluationRequestDTO(); + requestDTO.setTargetAssetId(assetIdStr); + evaluatePolicyForAsset(policy.getId(), requestDTO, eventInitiatorId); + + } catch (Exception e) { + log.error("Error during evaluation of policy {} for asset {}: {}", policy.getName(), assetIdStr, e.getMessage(), e); + } + } + log.info("Finished processing asset change event for assetId: {}", assetIdStr); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/service/PolicyService.java b/src/main/java/com/dalab/policyengine/service/PolicyService.java new file mode 100644 index 0000000000000000000000000000000000000000..d2780795d07acc3ee6ddaf69afefca67be776e41 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/service/PolicyService.java @@ -0,0 +1,834 @@ +package com.dalab.policyengine.service; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import com.dalab.policyengine.common.ConflictException; +import com.dalab.policyengine.common.ResourceNotFoundException; +import com.dalab.policyengine.dto.PolicyDraftActionDTO; +import com.dalab.policyengine.dto.PolicyDraftInputDTO; +import com.dalab.policyengine.dto.PolicyDraftOutputDTO; +import com.dalab.policyengine.dto.PolicyImpactRequestDTO; +import com.dalab.policyengine.dto.PolicyImpactResponseDTO; +// Add missing DTO imports +import com.dalab.policyengine.dto.PolicyInputDTO; +import com.dalab.policyengine.dto.PolicyOutputDTO; +import com.dalab.policyengine.dto.PolicySummaryDTO; +import com.dalab.policyengine.mapper.PolicyDraftMapper; +// Add missing service imports +import com.dalab.policyengine.mapper.PolicyMapper; +// Add missing entity imports +import com.dalab.policyengine.model.Policy; +import com.dalab.policyengine.model.PolicyDraft; +import com.dalab.policyengine.model.PolicyDraftStatus; +import com.dalab.policyengine.model.PolicyStatus; +import com.dalab.policyengine.repository.PolicyDraftRepository; +import com.dalab.policyengine.repository.PolicyRepository; + +// Add missing JPA criteria import +import jakarta.persistence.criteria.Predicate; + +@Service +@Transactional +public class PolicyService implements IPolicyService { + + private static final Logger log = LoggerFactory.getLogger(PolicyService.class); + + private final PolicyRepository policyRepository; + private final PolicyMapper policyMapper; + + // Draft management dependencies + private final PolicyDraftRepository policyDraftRepository; + private final PolicyDraftMapper policyDraftMapper; + + @Autowired + public PolicyService(PolicyRepository policyRepository, PolicyMapper policyMapper, + PolicyDraftRepository policyDraftRepository, PolicyDraftMapper policyDraftMapper) { + this.policyRepository = policyRepository; + this.policyMapper = policyMapper; + this.policyDraftRepository = policyDraftRepository; + this.policyDraftMapper = policyDraftMapper; + } + + @Override + @Transactional(readOnly = true) + public Page getAllPolicies(Pageable pageable, String status, String nameContains) { + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + if (StringUtils.hasText(status)) { + try { + predicates.add(criteriaBuilder.equal(root.get("status"), PolicyStatus.valueOf(status.toUpperCase()))); + } catch (IllegalArgumentException e) { + log.warn("Invalid policy status provided: {}", status); + // Option: throw bad request, or ignore filter, or return empty + // Returning empty for now by adding a certainly false predicate + predicates.add(criteriaBuilder.disjunction()); + } + } + if (StringUtils.hasText(nameContains)) { + predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), + "%" + nameContains.toLowerCase() + "%")); + } + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + return policyRepository.findAll(spec, pageable).map(policyMapper::toPolicySummaryDTO); + } + + @Override + @Transactional(readOnly = true) + public PolicyOutputDTO getPolicyById(UUID policyId) { + Policy policy = policyRepository.findById(policyId) + .orElseThrow(() -> new ResourceNotFoundException("Policy", "id", policyId.toString())); + return policyMapper.toPolicyOutputDTO(policy); + } + + @Override + public PolicyOutputDTO createPolicy(PolicyInputDTO policyInputDTO, UUID creatorUserId) { + policyRepository.findByName(policyInputDTO.getName()).ifPresent(p -> { + throw new ConflictException("Policy with name '" + policyInputDTO.getName() + "' already exists."); + }); + + Policy policy = policyMapper.toPolicyEntity(policyInputDTO); + policy.setCreatedByUserId(creatorUserId); + // createdAt and updatedAt are set by @PrePersist + + Policy savedPolicy = policyRepository.save(policy); + log.info("Created policy {} with id {}", savedPolicy.getName(), savedPolicy.getId()); + return policyMapper.toPolicyOutputDTO(savedPolicy); + } + + @Override + public PolicyOutputDTO updatePolicy(UUID policyId, PolicyInputDTO policyInputDTO, UUID updaterUserId) { + Policy existingPolicy = policyRepository.findById(policyId) + .orElseThrow(() -> new ResourceNotFoundException("Policy", "id", policyId.toString())); + + // Check for name conflict if name is being changed + if (!existingPolicy.getName().equals(policyInputDTO.getName())) { + policyRepository.findByName(policyInputDTO.getName()).ifPresent(p -> { + if (!p.getId().equals(existingPolicy.getId())) { + throw new ConflictException("Another policy with name '" + policyInputDTO.getName() + "' already exists."); + } + }); + } + + policyMapper.updatePolicyEntityFromInputDTO(existingPolicy, policyInputDTO); + existingPolicy.setUpdatedByUserId(updaterUserId); + // updatedAt is set by @PreUpdate + + Policy updatedPolicy = policyRepository.save(existingPolicy); + log.info("Updated policy {} with id {}", updatedPolicy.getName(), updatedPolicy.getId()); + return policyMapper.toPolicyOutputDTO(updatedPolicy); + } + + @Override + public void deletePolicy(UUID policyId) { + if (!policyRepository.existsById(policyId)) { + throw new ResourceNotFoundException("Policy", "id", policyId.toString()); + } + // Consider implications: what about active evaluations? Soft delete? + // For now, direct delete. + policyRepository.deleteById(policyId); + log.info("Deleted policy with id {}", policyId); + } + + @Override + @Transactional(readOnly = true) + public PolicyImpactResponseDTO analyzePolicy(PolicyImpactRequestDTO request) { + log.info("Analyzing policy impact for rules content with analysis type: {}", request.getAnalysisType()); + + long startTime = System.currentTimeMillis(); + + // Generate unique analysis ID + String analysisId = "impact-" + UUID.randomUUID().toString().substring(0, 8); + + // Create response with comprehensive mock data + PolicyImpactResponseDTO response = new PolicyImpactResponseDTO(analysisId, request.getAnalysisType()); + + // Build impact summary based on analysis type + PolicyImpactResponseDTO.ImpactSummaryDTO summary = createImpactSummary(request.getAnalysisType()); + response.setSummary(summary); + + // Build affected assets list + List affectedAssets = createAffectedAssetsList(request); + response.setAffectedAssets(affectedAssets); + + // Add performance impact if requested + if (Boolean.TRUE.equals(request.getIncludePerformanceEstimate())) { + PolicyImpactResponseDTO.PerformanceImpactDTO performanceImpact = createPerformanceImpact(request.getAnalysisType()); + response.setPerformanceImpact(performanceImpact); + } + + // Add cost impact if requested + if (Boolean.TRUE.equals(request.getIncludeCostImpact())) { + PolicyImpactResponseDTO.CostImpactDTO costImpact = createCostImpact(request.getAnalysisType()); + response.setCostImpact(costImpact); + } + + // Add compliance impact if requested + if (Boolean.TRUE.equals(request.getIncludeComplianceImpact())) { + PolicyImpactResponseDTO.ComplianceImpactDTO complianceImpact = createComplianceImpact(); + response.setComplianceImpact(complianceImpact); + } + + // Build risk assessment + PolicyImpactResponseDTO.RiskAssessmentDTO riskAssessment = createRiskAssessment(summary); + response.setRiskAssessment(riskAssessment); + + // Build recommendations + List recommendations = createRecommendations(request, summary); + response.setRecommendations(recommendations); + + // Build metadata + long executionTime = System.currentTimeMillis() - startTime; + PolicyImpactResponseDTO.AnalysisMetadataDTO metadata = createAnalysisMetadata(executionTime); + response.setMetadata(metadata); + + log.info("Policy impact analysis completed in {}ms for analysis ID: {}", executionTime, analysisId); + return response; + } + + /** + * Create impact summary based on analysis type with realistic metrics. + */ + private PolicyImpactResponseDTO.ImpactSummaryDTO createImpactSummary(String analysisType) { + PolicyImpactResponseDTO.ImpactSummaryDTO summary = new PolicyImpactResponseDTO.ImpactSummaryDTO(); + + switch (analysisType.toUpperCase()) { + case "FULL": + summary.setTotalAssetsAnalyzed(2847); + summary.setTotalAssetsAffected(1234); + summary.setHighImpactAssets(89); + summary.setMediumImpactAssets(456); + summary.setLowImpactAssets(689); + summary.setOverallRiskLevel("MEDIUM"); + summary.setImpactPercentage(43.4); + break; + case "QUICK": + summary.setTotalAssetsAnalyzed(500); + summary.setTotalAssetsAffected(218); + summary.setHighImpactAssets(15); + summary.setMediumImpactAssets(78); + summary.setLowImpactAssets(125); + summary.setOverallRiskLevel("LOW"); + summary.setImpactPercentage(43.6); + break; + case "TARGETED": + summary.setTotalAssetsAnalyzed(156); + summary.setTotalAssetsAffected(89); + summary.setHighImpactAssets(12); + summary.setMediumImpactAssets(34); + summary.setLowImpactAssets(43); + summary.setOverallRiskLevel("HIGH"); + summary.setImpactPercentage(57.1); + break; + default: + summary.setTotalAssetsAnalyzed(1000); + summary.setTotalAssetsAffected(420); + summary.setHighImpactAssets(35); + summary.setMediumImpactAssets(150); + summary.setLowImpactAssets(235); + summary.setOverallRiskLevel("MEDIUM"); + summary.setImpactPercentage(42.0); + } + + return summary; + } + + /** + * Create list of affected assets with detailed impact information. + */ + private List createAffectedAssetsList(PolicyImpactRequestDTO request) { + List assets = new ArrayList<>(); + + // Sample high-impact asset + PolicyImpactResponseDTO.AssetImpactDTO highImpactAsset = new PolicyImpactResponseDTO.AssetImpactDTO(); + highImpactAsset.setAssetId("asset-db-customer-001"); + highImpactAsset.setAssetName("Customer Database - Production"); + highImpactAsset.setAssetType("database"); + highImpactAsset.setImpactLevel("HIGH"); + highImpactAsset.setAffectedAttributes(Arrays.asList("personal_data", "financial_data", "contact_info")); + highImpactAsset.setAppliedActions(Arrays.asList("auto_encrypt", "access_log", "compliance_tag")); + highImpactAsset.setRiskAssessment("High business impact due to customer data sensitivity and compliance requirements"); + + Map highImpactDetails = new HashMap<>(); + highImpactDetails.put("recordCount", 1250000); + highImpactDetails.put("dataTypes", Arrays.asList("PII", "Financial", "Health")); + highImpactDetails.put("complianceFrameworks", Arrays.asList("GDPR", "PCI-DSS", "HIPAA")); + highImpactAsset.setImpactDetails(highImpactDetails); + assets.add(highImpactAsset); + + // Sample medium-impact asset + PolicyImpactResponseDTO.AssetImpactDTO mediumImpactAsset = new PolicyImpactResponseDTO.AssetImpactDTO(); + mediumImpactAsset.setAssetId("asset-file-logs-002"); + mediumImpactAsset.setAssetName("Application Logs - Analytics"); + mediumImpactAsset.setAssetType("file"); + mediumImpactAsset.setImpactLevel("MEDIUM"); + mediumImpactAsset.setAffectedAttributes(Arrays.asList("user_sessions", "performance_metrics")); + mediumImpactAsset.setAppliedActions(Arrays.asList("retention_policy", "anonymize")); + mediumImpactAsset.setRiskAssessment("Moderate impact on analytics capabilities with potential performance implications"); + + Map mediumImpactDetails = new HashMap<>(); + mediumImpactDetails.put("fileSizeGB", 45.7); + mediumImpactDetails.put("retentionPeriodDays", 90); + mediumImpactDetails.put("accessFrequency", "daily"); + mediumImpactAsset.setImpactDetails(mediumImpactDetails); + assets.add(mediumImpactAsset); + + // Sample low-impact asset + PolicyImpactResponseDTO.AssetImpactDTO lowImpactAsset = new PolicyImpactResponseDTO.AssetImpactDTO(); + lowImpactAsset.setAssetId("asset-api-public-003"); + lowImpactAsset.setAssetName("Public API Documentation"); + lowImpactAsset.setAssetType("api"); + lowImpactAsset.setImpactLevel("LOW"); + lowImpactAsset.setAffectedAttributes(Arrays.asList("endpoint_metadata")); + lowImpactAsset.setAppliedActions(Arrays.asList("classification_tag")); + lowImpactAsset.setRiskAssessment("Minimal impact on public-facing documentation"); + + Map lowImpactDetails = new HashMap<>(); + lowImpactDetails.put("endpointCount", 127); + lowImpactDetails.put("publicAccess", true); + lowImpactDetails.put("lastUpdated", "2024-12-15"); + lowImpactAsset.setImpactDetails(lowImpactDetails); + assets.add(lowImpactAsset); + + return assets; + } + + /** + * Create performance impact estimates based on analysis type. + */ + private PolicyImpactResponseDTO.PerformanceImpactDTO createPerformanceImpact(String analysisType) { + PolicyImpactResponseDTO.PerformanceImpactDTO performance = new PolicyImpactResponseDTO.PerformanceImpactDTO(); + + switch (analysisType.toUpperCase()) { + case "FULL": + performance.setEstimatedProcessingTimeMs(45000L); + performance.setCpuUtilizationIncrease(15.7); + performance.setMemoryUtilizationIncrease(8.3); + performance.setEstimatedApiCalls(2847); + performance.setPerformanceRiskLevel("MEDIUM"); + break; + case "QUICK": + performance.setEstimatedProcessingTimeMs(5200L); + performance.setCpuUtilizationIncrease(3.2); + performance.setMemoryUtilizationIncrease(2.1); + performance.setEstimatedApiCalls(350); + performance.setPerformanceRiskLevel("LOW"); + break; + case "TARGETED": + performance.setEstimatedProcessingTimeMs(8500L); + performance.setCpuUtilizationIncrease(5.8); + performance.setMemoryUtilizationIncrease(3.4); + performance.setEstimatedApiCalls(156); + performance.setPerformanceRiskLevel("LOW"); + break; + default: + performance.setEstimatedProcessingTimeMs(15000L); + performance.setCpuUtilizationIncrease(7.5); + performance.setMemoryUtilizationIncrease(4.2); + performance.setEstimatedApiCalls(1000); + performance.setPerformanceRiskLevel("MEDIUM"); + } + + return performance; + } + + /** + * Create cost impact analysis with estimated costs and savings. + */ + private PolicyImpactResponseDTO.CostImpactDTO createCostImpact(String analysisType) { + PolicyImpactResponseDTO.CostImpactDTO cost = new PolicyImpactResponseDTO.CostImpactDTO(); + + cost.setEstimatedMonthlyCost(2847.50); + cost.setEstimatedImplementationCost(15750.00); + cost.setPotentialSavings(8450.25); + cost.setCostRiskLevel("MEDIUM"); + + Map breakdown = new HashMap<>(); + breakdown.put("compute_resources", 1200.00); + breakdown.put("storage_costs", 675.50); + breakdown.put("api_calls", 425.75); + breakdown.put("compliance_tools", 546.25); + cost.setCostBreakdown(breakdown); + + return cost; + } + + /** + * Create compliance impact analysis. + */ + private PolicyImpactResponseDTO.ComplianceImpactDTO createComplianceImpact() { + PolicyImpactResponseDTO.ComplianceImpactDTO compliance = new PolicyImpactResponseDTO.ComplianceImpactDTO(); + + compliance.setConflictingPolicies(2); + compliance.setComplianceFrameworksAffected(Arrays.asList("GDPR", "PCI-DSS", "SOX", "HIPAA")); + compliance.setComplianceRiskLevel("MEDIUM"); + compliance.setPotentialViolations(Arrays.asList( + "Data retention period conflict with existing archival policy", + "Encryption requirements may override current security policy" + )); + + return compliance; + } + + /** + * Create comprehensive risk assessment. + */ + private PolicyImpactResponseDTO.RiskAssessmentDTO createRiskAssessment(PolicyImpactResponseDTO.ImpactSummaryDTO summary) { + PolicyImpactResponseDTO.RiskAssessmentDTO risk = new PolicyImpactResponseDTO.RiskAssessmentDTO(); + + risk.setOverallRiskLevel(summary.getOverallRiskLevel()); + risk.setRiskScore(calculateRiskScore(summary)); + + List risks = Arrays.asList( + "High-volume data processing may impact system performance", + "Compliance conflicts require manual review and resolution", + "Implementation costs exceed initial estimates", + "Customer data access patterns may be disrupted" + ); + risk.setIdentifiedRisks(risks); + + List mitigations = Arrays.asList( + "Implement gradual rollout with performance monitoring", + "Conduct compliance review before full deployment", + "Establish cost monitoring and alerting mechanisms", + "Create customer communication plan for access changes" + ); + risk.setMitigationStrategies(mitigations); + + return risk; + } + + /** + * Calculate numeric risk score based on impact summary. + */ + private Double calculateRiskScore(PolicyImpactResponseDTO.ImpactSummaryDTO summary) { + double baseScore = 50.0; + + // Adjust based on impact level distribution + if (summary.getHighImpactAssets() != null) { + baseScore += summary.getHighImpactAssets() * 0.5; + } + if (summary.getMediumImpactAssets() != null) { + baseScore += summary.getMediumImpactAssets() * 0.2; + } + + // Adjust based on overall impact percentage + if (summary.getImpactPercentage() != null) { + baseScore += (summary.getImpactPercentage() - 30.0) * 0.8; + } + + // Cap the score between 0 and 100 + return Math.max(0.0, Math.min(100.0, baseScore)); + } + + /** + * Create implementation recommendations based on analysis. + */ + private List createRecommendations(PolicyImpactRequestDTO request, PolicyImpactResponseDTO.ImpactSummaryDTO summary) { + List recommendations = new ArrayList<>(); + + // Risk-based recommendations + switch (summary.getOverallRiskLevel()) { + case "HIGH": + recommendations.add("Consider phased implementation starting with low-impact assets"); + recommendations.add("Establish comprehensive rollback procedures"); + recommendations.add("Increase monitoring and alerting during implementation"); + break; + case "MEDIUM": + recommendations.add("Implement with standard change management procedures"); + recommendations.add("Monitor key performance indicators during rollout"); + break; + case "LOW": + recommendations.add("Proceed with standard implementation timeline"); + recommendations.add("Standard post-implementation review recommended"); + break; + } + + // Analysis type specific recommendations + if ("QUICK".equals(request.getAnalysisType())) { + recommendations.add("Consider running FULL analysis before production deployment"); + } + + // Performance-based recommendations + if (Boolean.TRUE.equals(request.getIncludePerformanceEstimate())) { + recommendations.add("Schedule implementation during low-traffic periods"); + recommendations.add("Prepare additional compute resources for initial processing"); + } + + return recommendations; + } + + /** + * Create analysis execution metadata. + */ + private PolicyImpactResponseDTO.AnalysisMetadataDTO createAnalysisMetadata(long executionTime) { + PolicyImpactResponseDTO.AnalysisMetadataDTO metadata = new PolicyImpactResponseDTO.AnalysisMetadataDTO(); + + metadata.setExecutionTimeMs(executionTime); + metadata.setAnalysisVersion("v2.4.1"); + metadata.setUsesCachedData(false); + metadata.setDataFreshnessTimestamp(Instant.now().minusSeconds(300)); // 5 minutes ago + + return metadata; + } + + // ========== POLICY DRAFT MANAGEMENT IMPLEMENTATION ========== + + @Override + @Transactional + public PolicyDraftOutputDTO createDraft(PolicyDraftInputDTO draftInput, UUID creatorUserId) { + log.info("Creating new policy draft: {}", draftInput.getName()); + + // Validate draft name uniqueness + if (policyDraftRepository.existsByName(draftInput.getName())) { + throw new ConflictException("A draft with this name already exists"); + } + + // Convert DTO to entity + PolicyDraft draft = policyDraftMapper.toEntity(draftInput); + draft.setCreatedByUserId(creatorUserId); + draft.setStatus(PolicyDraftStatus.CREATED); + + // Save the draft + PolicyDraft savedDraft = policyDraftRepository.save(draft); + + log.info("Successfully created policy draft with ID: {}", savedDraft.getId()); + return policyDraftMapper.toOutputDTO(savedDraft); + } + + @Override + @Transactional(readOnly = true) + public PolicyDraftOutputDTO getDraftById(UUID draftId) { + log.debug("Retrieving policy draft with ID: {}", draftId); + + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + return policyDraftMapper.toOutputDTO(draft); + } + + @Override + @Transactional + public PolicyDraftOutputDTO updateDraft(UUID draftId, PolicyDraftInputDTO draftInput, UUID updaterUserId) { + log.info("Updating policy draft with ID: {}", draftId); + + PolicyDraft existingDraft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + // Validate that draft can be updated + if (!canEditDraft(existingDraft.getStatus())) { + throw new ConflictException("Draft cannot be edited in current status: " + existingDraft.getStatus()); + } + + // Validate name uniqueness (excluding current draft) + if (!draftInput.getName().equals(existingDraft.getName()) && + policyDraftRepository.existsByNameExcludingId(draftInput.getName(), draftId)) { + throw new ConflictException("A draft with this name already exists"); + } + + // Update the draft + policyDraftMapper.updateEntity(existingDraft, draftInput); + existingDraft.setUpdatedByUserId(updaterUserId); + + PolicyDraft savedDraft = policyDraftRepository.save(existingDraft); + + log.info("Successfully updated policy draft with ID: {}", savedDraft.getId()); + return policyDraftMapper.toOutputDTO(savedDraft); + } + + @Override + @Transactional + public PolicyDraftOutputDTO submitDraft(UUID draftId, PolicyDraftActionDTO action, UUID submittedByUserId) { + log.info("Submitting policy draft with ID: {}", draftId); + + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + // Validate status transition + if (!canSubmitDraft(draft.getStatus())) { + throw new ConflictException("Draft cannot be submitted in current status: " + draft.getStatus()); + } + + // Update draft status and metadata + draft.setStatus(PolicyDraftStatus.SUBMITTED); + draft.setSubmittedAt(Instant.now()); + draft.setSubmittedByUserId(submittedByUserId); + draft.setUpdatedByUserId(submittedByUserId); + + // Add comment if provided + if (action.getComment() != null && !action.getComment().trim().isEmpty()) { + draft.addReviewComment("Submission: " + action.getComment(), submittedByUserId, "SUBMITTER"); + } + + PolicyDraft savedDraft = policyDraftRepository.save(draft); + + log.info("Successfully submitted policy draft with ID: {}", savedDraft.getId()); + return policyDraftMapper.toOutputDTO(savedDraft); + } + + @Override + @Transactional + public PolicyDraftOutputDTO approveDraft(UUID draftId, PolicyDraftActionDTO action, UUID approverUserId) { + log.info("Approving policy draft with ID: {}", draftId); + + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + // Validate status transition + if (!canApproveDraft(draft.getStatus())) { + throw new ConflictException("Draft cannot be approved in current status: " + draft.getStatus()); + } + + // Update draft status and metadata + draft.setStatus(PolicyDraftStatus.APPROVED); + draft.setApprovedAt(Instant.now()); + draft.setApprovedByUserId(approverUserId); + draft.setUpdatedByUserId(approverUserId); + + // Add comment if provided + if (action.getComment() != null && !action.getComment().trim().isEmpty()) { + draft.addReviewComment("Approval: " + action.getComment(), approverUserId, "APPROVER"); + } + + PolicyDraft savedDraft = policyDraftRepository.save(draft); + + log.info("Successfully approved policy draft with ID: {}", savedDraft.getId()); + return policyDraftMapper.toOutputDTO(savedDraft); + } + + @Override + @Transactional + public PolicyDraftOutputDTO rejectDraft(UUID draftId, PolicyDraftActionDTO action, UUID rejectorUserId) { + log.info("Rejecting policy draft with ID: {}", draftId); + + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + // Validate status transition + if (!canRejectDraft(draft.getStatus())) { + throw new ConflictException("Draft cannot be rejected in current status: " + draft.getStatus()); + } + + // Rejection requires a comment + if (action.getComment() == null || action.getComment().trim().isEmpty()) { + throw new IllegalArgumentException("Comment is required for draft rejection"); + } + + // Update draft status and metadata + draft.setStatus(PolicyDraftStatus.REJECTED); + draft.setRejectedAt(Instant.now()); + draft.setRejectedByUserId(rejectorUserId); + draft.setUpdatedByUserId(rejectorUserId); + + // Add rejection comment + draft.addReviewComment("Rejection: " + action.getComment(), rejectorUserId, "REJECTOR"); + + PolicyDraft savedDraft = policyDraftRepository.save(draft); + + log.info("Successfully rejected policy draft with ID: {}", savedDraft.getId()); + return policyDraftMapper.toOutputDTO(savedDraft); + } + + @Override + @Transactional + public PolicyDraftOutputDTO requestChanges(UUID draftId, PolicyDraftActionDTO action, UUID reviewerUserId) { + log.info("Requesting changes for policy draft with ID: {}", draftId); + + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + // Validate status transition + if (!canRequestChanges(draft.getStatus())) { + throw new ConflictException("Changes cannot be requested for draft in current status: " + draft.getStatus()); + } + + // Change request requires a comment + if (action.getComment() == null || action.getComment().trim().isEmpty()) { + throw new IllegalArgumentException("Comment is required for change request"); + } + + // Update draft status and metadata + draft.setStatus(PolicyDraftStatus.REQUIRES_CHANGES); + draft.setUpdatedByUserId(reviewerUserId); + + // Add change request comment + draft.addReviewComment("Change Request: " + action.getComment(), reviewerUserId, "REVIEWER"); + + PolicyDraft savedDraft = policyDraftRepository.save(draft); + + log.info("Successfully requested changes for policy draft with ID: {}", savedDraft.getId()); + return policyDraftMapper.toOutputDTO(savedDraft); + } + + @Override + @Transactional + public PolicyOutputDTO publishDraft(UUID draftId, PolicyDraftActionDTO action, UUID publisherUserId) { + log.info("Publishing policy draft with ID: {}", draftId); + + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + // Validate status + if (draft.getStatus() != PolicyDraftStatus.APPROVED) { + throw new ConflictException("Only approved drafts can be published"); + } + + // Create policy from draft + PolicyInputDTO policyInput = convertDraftToPolicy(draft); + PolicyOutputDTO publishedPolicy = createPolicy(policyInput, publisherUserId); + + // Update draft status + draft.setStatus(PolicyDraftStatus.PUBLISHED); + draft.setPublishedAt(Instant.now()); + draft.setPublishedByUserId(publisherUserId); + draft.setUpdatedByUserId(publisherUserId); + + // Add publication comment + if (action.getComment() != null && !action.getComment().trim().isEmpty()) { + draft.addReviewComment("Publication: " + action.getComment(), publisherUserId, "PUBLISHER"); + } + + policyDraftRepository.save(draft); + + log.info("Successfully published policy draft {} as policy {}", draftId, publishedPolicy.getId()); + return publishedPolicy; + } + + // Helper methods for validation + private boolean canEditDraft(PolicyDraftStatus status) { + return status == PolicyDraftStatus.CREATED || status == PolicyDraftStatus.REQUIRES_CHANGES; + } + + private boolean canSubmitDraft(PolicyDraftStatus status) { + return status == PolicyDraftStatus.CREATED || status == PolicyDraftStatus.REQUIRES_CHANGES; + } + + private boolean canApproveDraft(PolicyDraftStatus status) { + return status == PolicyDraftStatus.SUBMITTED || status == PolicyDraftStatus.UNDER_REVIEW; + } + + private boolean canRejectDraft(PolicyDraftStatus status) { + return status == PolicyDraftStatus.SUBMITTED || + status == PolicyDraftStatus.UNDER_REVIEW || + status == PolicyDraftStatus.APPROVED; + } + + private boolean canRequestChanges(PolicyDraftStatus status) { + return status == PolicyDraftStatus.SUBMITTED || status == PolicyDraftStatus.UNDER_REVIEW; + } + + // Convert draft to policy for publication + private PolicyInputDTO convertDraftToPolicy(PolicyDraft draft) { + PolicyInputDTO policyInput = new PolicyInputDTO(); + policyInput.setName(draft.getName()); + policyInput.setDescription(draft.getDescription()); + policyInput.setConditionLogic(draft.getConditionLogic()); + policyInput.setActions(draft.getActions()); + + // Convert rules definition to PolicyRule format if needed + // This is simplified - real implementation would convert JSON rules to PolicyRule entities + + return policyInput; + } + + // Placeholder implementations for remaining methods (to resolve compilation errors) + @Override + public Page getAllDrafts(Pageable pageable, PolicyDraftStatus status, String category, String priority, UUID createdBy, String nameContains) { + // TODO: Implement comprehensive search with filters + Page drafts = policyDraftRepository.findAll(pageable); + return drafts.map(policyDraftMapper::toOutputDTO); + } + + @Override + public Page getDraftsRequiringAttention(UUID userId, Pageable pageable) { + Page drafts = policyDraftRepository.findRequiringAttention(userId, pageable); + return drafts.map(policyDraftMapper::toOutputDTO); + } + + @Override + public Page getDraftsPendingReview(Pageable pageable) { + Page drafts = policyDraftRepository.findPendingReview(pageable); + return drafts.map(policyDraftMapper::toOutputDTO); + } + + @Override + public PolicyDraftOutputDTO archiveDraft(UUID draftId, PolicyDraftActionDTO action, UUID archiverUserId) { + // TODO: Implement archive functionality + throw new UnsupportedOperationException("Archive functionality not yet implemented"); + } + + @Override + public PolicyDraftOutputDTO addReviewComment(UUID draftId, String comment, UUID reviewerUserId, String reviewerRole) { + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + draft.addReviewComment(comment, reviewerUserId, reviewerRole); + PolicyDraft savedDraft = policyDraftRepository.save(draft); + + return policyDraftMapper.toOutputDTO(savedDraft); + } + + @Override + public void deleteDraft(UUID draftId, UUID deleterUserId) { + PolicyDraft draft = policyDraftRepository.findById(draftId) + .orElseThrow(() -> new ResourceNotFoundException("Policy draft not found with ID: " + draftId)); + + if (draft.getStatus() != PolicyDraftStatus.CREATED) { + throw new ConflictException("Only drafts in CREATED status can be deleted"); + } + + policyDraftRepository.delete(draft); + log.info("Successfully deleted policy draft with ID: {}", draftId); + } + + @Override + public List getDraftCategories() { + return policyDraftRepository.findAllCategories(); + } + + @Override + public List getDraftTags() { + return policyDraftRepository.findAllTags(); + } + + @Override + public List getDraftStatistics() { + return policyDraftRepository.getDraftStatisticsByStatus(); + } + + @Override + public List getOverdueDrafts() { + List overdueDrafts = policyDraftRepository.findOverdueDrafts(Instant.now()); + return overdueDrafts.stream() + .map(policyDraftMapper::toOutputDTO) + .collect(java.util.stream.Collectors.toList()); + } + + @Override + public PolicyDraftOutputDTO clonePolicyAsDraft(UUID policyId, UUID creatorUserId) { + // TODO: Implement policy cloning functionality + throw new UnsupportedOperationException("Policy cloning functionality not yet implemented"); + } + + @Override + public PolicyDraftOutputDTO createDraftVersion(UUID draftId, PolicyDraftInputDTO draftInput, UUID creatorUserId) { + // TODO: Implement versioning functionality + throw new UnsupportedOperationException("Draft versioning functionality not yet implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/web/rest/EventCenterController.java b/src/main/java/com/dalab/policyengine/web/rest/EventCenterController.java new file mode 100644 index 0000000000000000000000000000000000000000..7c9c5db873d0148180a761ae57ddbf64e3902bc6 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/web/rest/EventCenterController.java @@ -0,0 +1,331 @@ +package com.dalab.policyengine.web.rest; + +import java.net.URI; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import com.dalab.common.security.SecurityUtils; +import com.dalab.policyengine.dto.EventAnalyticsDTO; +import com.dalab.policyengine.dto.EventStreamDTO; +import com.dalab.policyengine.dto.EventSubscriptionInputDTO; +import com.dalab.policyengine.dto.EventSubscriptionOutputDTO; +import com.dalab.policyengine.model.EventSubscriptionStatus; +import com.dalab.policyengine.model.EventType; +import com.dalab.policyengine.service.IEventSubscriptionService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +/** + * REST controller for Event Center functionality. + * Provides endpoints for managing event subscriptions, streaming events, and analytics. + */ +@RestController +@RequestMapping("/api/v1/policyengine/events") +@Tag(name = "Event Center", description = "Endpoints for Event Center management, streaming, and analytics") +public class EventCenterController { + + private static final Logger log = LoggerFactory.getLogger(EventCenterController.class); + + private final IEventSubscriptionService eventSubscriptionService; + + @Autowired + public EventCenterController(IEventSubscriptionService eventSubscriptionService) { + this.eventSubscriptionService = eventSubscriptionService; + } + + // Endpoint 1: Subscription Management + @PostMapping("/subscriptions") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Create event subscription", + description = "Create a new event subscription to receive notifications for specific event types and conditions" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Event subscription created successfully"), + @ApiResponse(responseCode = "400", description = "Invalid subscription configuration"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to create subscription") + }) + public ResponseEntity createSubscription( + @Parameter(description = "Event subscription configuration") + @Valid @RequestBody EventSubscriptionInputDTO inputDTO) { + log.info("REST request to create Event Subscription: {}", inputDTO.getName()); + + UUID creatorUserId = SecurityUtils.getAuthenticatedUserId(); + EventSubscriptionOutputDTO createdSubscription = eventSubscriptionService.createSubscription(inputDTO, creatorUserId); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(createdSubscription.getId()) + .toUri(); + + return ResponseEntity.created(location).body(createdSubscription); + } + + @GetMapping("/subscriptions") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Get user's event subscriptions", + description = "Retrieve event subscriptions for the authenticated user with optional filtering" + ) + public ResponseEntity> getUserSubscriptions( + @PageableDefault(size = 20, sort = "name") Pageable pageable, + @RequestParam(required = false) String status, + @RequestParam(required = false) String nameContains) { + log.info("REST request to get Event Subscriptions for user with filters: status={}, nameContains={}", status, nameContains); + + UUID userId = SecurityUtils.getAuthenticatedUserId(); + Page subscriptionsPage = eventSubscriptionService.getSubscriptionsForUser( + userId, pageable, status, nameContains); + + return ResponseEntity.ok(subscriptionsPage); + } + + @GetMapping("/subscriptions/{subscriptionId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Get event subscription by ID", + description = "Retrieve detailed information about a specific event subscription" + ) + public ResponseEntity getSubscriptionById(@PathVariable UUID subscriptionId) { + log.info("REST request to get Event Subscription by id: {}", subscriptionId); + + EventSubscriptionOutputDTO subscription = eventSubscriptionService.getSubscriptionById(subscriptionId); + return ResponseEntity.ok(subscription); + } + + @PutMapping("/subscriptions/{subscriptionId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Update event subscription", + description = "Update configuration of an existing event subscription" + ) + public ResponseEntity updateSubscription( + @PathVariable UUID subscriptionId, + @Valid @RequestBody EventSubscriptionInputDTO inputDTO) { + log.info("REST request to update Event Subscription: {}", subscriptionId); + + UUID updaterUserId = SecurityUtils.getAuthenticatedUserId(); + EventSubscriptionOutputDTO updatedSubscription = eventSubscriptionService.updateSubscription( + subscriptionId, inputDTO, updaterUserId); + + return ResponseEntity.ok(updatedSubscription); + } + + @DeleteMapping("/subscriptions/{subscriptionId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Delete event subscription", + description = "Delete an event subscription and stop receiving notifications" + ) + public ResponseEntity deleteSubscription(@PathVariable UUID subscriptionId) { + log.info("REST request to delete Event Subscription: {}", subscriptionId); + + eventSubscriptionService.deleteSubscription(subscriptionId); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/subscriptions/{subscriptionId}/status") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Update subscription status", + description = "Enable, disable, pause, or archive an event subscription" + ) + public ResponseEntity updateSubscriptionStatus( + @PathVariable UUID subscriptionId, + @RequestParam EventSubscriptionStatus status) { + log.info("REST request to update Event Subscription status: {} to {}", subscriptionId, status); + + UUID updaterUserId = SecurityUtils.getAuthenticatedUserId(); + EventSubscriptionOutputDTO updatedSubscription = eventSubscriptionService.updateSubscriptionStatus( + subscriptionId, status, updaterUserId); + + return ResponseEntity.ok(updatedSubscription); + } + + // Endpoint 2: Event Streaming + @GetMapping("/stream") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Get real-time event stream", + description = "Retrieve real-time events matching user's subscriptions for the Event Center dashboard" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Event stream retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to access event stream") + }) + public ResponseEntity> getEventStream( + @Parameter(description = "Maximum number of events to return (default: 50)") + @RequestParam(defaultValue = "50") Integer limit) { + log.info("REST request to get Event Stream with limit: {}", limit); + + UUID userId = SecurityUtils.getAuthenticatedUserId(); + List eventStream = eventSubscriptionService.getEventStreamForUser(userId, limit); + + return ResponseEntity.ok(eventStream); + } + + @GetMapping("/stream/all") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + @Operation( + summary = "Get all event stream (Admin only)", + description = "Retrieve real-time events from all subscriptions across the platform" + ) + public ResponseEntity> getAllEventStream( + @RequestParam(defaultValue = "100") Integer limit) { + log.info("REST request to get All Event Stream with limit: {}", limit); + + List eventStream = eventSubscriptionService.getAllEventStream(limit); + return ResponseEntity.ok(eventStream); + } + + // Endpoint 3: Historical Events + @GetMapping("/subscriptions/{subscriptionId}/history") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Get historical events for subscription", + description = "Retrieve paginated historical events that matched a specific subscription" + ) + public ResponseEntity> getHistoricalEvents( + @PathVariable UUID subscriptionId, + @PageableDefault(size = 50, sort = "timestamp") Pageable pageable) { + log.info("REST request to get Historical Events for subscription: {}", subscriptionId); + + Page historicalEvents = eventSubscriptionService.getHistoricalEventsForSubscription( + subscriptionId, pageable); + + return ResponseEntity.ok(historicalEvents); + } + + // Endpoint 4: Event Analytics + @GetMapping("/analytics") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Get event analytics dashboard", + description = "Retrieve comprehensive analytics for user's event subscriptions including trends, metrics, and insights" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Event analytics retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to access analytics") + }) + public ResponseEntity getEventAnalytics( + @Parameter(description = "Start time for analytics (defaults to 24 hours ago)") + @RequestParam(required = false) String fromTime, + @Parameter(description = "End time for analytics (defaults to now)") + @RequestParam(required = false) String toTime) { + log.info("REST request to get Event Analytics with time range: {} to {}", fromTime, toTime); + + UUID userId = SecurityUtils.getAuthenticatedUserId(); + + if (fromTime != null && toTime != null) { + Instant from = Instant.parse(fromTime); + Instant to = Instant.parse(toTime); + EventAnalyticsDTO analytics = eventSubscriptionService.getEventAnalyticsForTimeRange(userId, from, to); + return ResponseEntity.ok(analytics); + } else { + EventAnalyticsDTO analytics = eventSubscriptionService.getEventAnalyticsForUser(userId); + return ResponseEntity.ok(analytics); + } + } + + @GetMapping("/analytics/system") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + @Operation( + summary = "Get system-wide event analytics (Admin only)", + description = "Retrieve comprehensive analytics across all subscriptions and users in the platform" + ) + public ResponseEntity getSystemEventAnalytics() { + log.info("REST request to get System Event Analytics"); + + EventAnalyticsDTO analytics = eventSubscriptionService.getSystemEventAnalytics(); + return ResponseEntity.ok(analytics); + } + + // Endpoint 5: Rule Testing + @PostMapping("/test-rule") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Test event rule condition", + description = "Test an event rule condition against sample event data to validate rule logic before creating subscription" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Rule test completed successfully"), + @ApiResponse(responseCode = "400", description = "Invalid rule condition or sample data") + }) + public ResponseEntity testEventRule( + @Parameter(description = "MVEL rule condition to test") + @RequestParam String ruleCondition, + @Parameter(description = "Sample event data for testing") + @Valid @RequestBody EventStreamDTO sampleEvent) { + log.info("REST request to test Event Rule condition: {}", ruleCondition); + + boolean ruleMatches = eventSubscriptionService.testEventRule(ruleCondition, sampleEvent); + + return ResponseEntity.ok(ruleMatches); + } + + // Endpoint 6: Configuration Helpers + @GetMapping("/config/event-types") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Get available event types", + description = "Retrieve list of all available event types for subscription configuration" + ) + public ResponseEntity> getAvailableEventTypes() { + log.info("REST request to get Available Event Types"); + + List eventTypes = eventSubscriptionService.getAvailableEventTypes(); + return ResponseEntity.ok(eventTypes); + } + + @GetMapping("/config/source-services") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Get available source services", + description = "Retrieve list of all available source services for subscription configuration" + ) + public ResponseEntity> getAvailableSourceServices() { + log.info("REST request to get Available Source Services"); + + List sourceServices = eventSubscriptionService.getAvailableSourceServices(); + return ResponseEntity.ok(sourceServices); + } + + @PostMapping("/config/validate") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + @Operation( + summary = "Validate subscription configuration", + description = "Validate event subscription configuration before creation to catch errors early" + ) + public ResponseEntity validateSubscriptionConfiguration( + @Valid @RequestBody EventSubscriptionInputDTO inputDTO) { + log.info("REST request to validate Subscription Configuration: {}", inputDTO.getName()); + + eventSubscriptionService.validateSubscriptionConfiguration(inputDTO); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/web/rest/PolicyController.java b/src/main/java/com/dalab/policyengine/web/rest/PolicyController.java new file mode 100644 index 0000000000000000000000000000000000000000..1860ac24f68a17cd1140d9052926cbd1f874054c --- /dev/null +++ b/src/main/java/com/dalab/policyengine/web/rest/PolicyController.java @@ -0,0 +1,140 @@ +package com.dalab.policyengine.web.rest; + +import java.net.URI; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import com.dalab.common.security.SecurityUtils; +import com.dalab.policyengine.dto.PolicyImpactRequestDTO; +import com.dalab.policyengine.dto.PolicyImpactResponseDTO; +import com.dalab.policyengine.dto.PolicyInputDTO; +import com.dalab.policyengine.dto.PolicyOutputDTO; +import com.dalab.policyengine.dto.PolicySummaryDTO; +import com.dalab.policyengine.service.IPolicyService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/v1/policyengine/policies") +@Tag(name = "Policy Management", description = "Endpoints for managing data governance policies") +public class PolicyController { + + private static final Logger log = LoggerFactory.getLogger(PolicyController.class); + + private final IPolicyService policyService; + + @Autowired + public PolicyController(IPolicyService policyService) { + this.policyService = policyService; + } + + @GetMapping + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + public ResponseEntity> getAllPolicies( + @PageableDefault(size = 20, sort = "name") Pageable pageable, + @RequestParam(required = false) String status, + @RequestParam(required = false) String nameContains) { + log.info("REST request to get all Policies with filters: status={}, nameContains={}", status, nameContains); + Page policyPage = policyService.getAllPolicies(pageable, status, nameContains); + return ResponseEntity.ok(policyPage); + } + + @GetMapping("/{policyId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_USER')") + public ResponseEntity getPolicyById(@PathVariable UUID policyId) { + log.info("REST request to get Policy by id: {}", policyId); + PolicyOutputDTO policyDTO = policyService.getPolicyById(policyId); + return ResponseEntity.ok(policyDTO); + } + + @PostMapping + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + public ResponseEntity createPolicy(@Valid @RequestBody PolicyInputDTO policyInputDTO) { + log.info("REST request to create Policy: {}", policyInputDTO.getName()); + + UUID creatorUserId = SecurityUtils.getAuthenticatedUserId(); + PolicyOutputDTO createdPolicy = policyService.createPolicy(policyInputDTO, creatorUserId); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(createdPolicy.getId()) + .toUri(); + + return ResponseEntity.created(location).body(createdPolicy); + } + + @PutMapping("/{policyId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + public ResponseEntity updatePolicy( + @PathVariable UUID policyId, + @Valid @RequestBody PolicyInputDTO policyInputDTO) { + log.info("REST request to update Policy: {}", policyId); + + UUID updaterUserId = SecurityUtils.getAuthenticatedUserId(); + PolicyOutputDTO updatedPolicy = policyService.updatePolicy(policyId, policyInputDTO, updaterUserId); + + return ResponseEntity.ok(updatedPolicy); + } + + @DeleteMapping("/{policyId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + public ResponseEntity deletePolicy(@PathVariable UUID policyId) { + log.info("REST request to delete Policy by id: {}", policyId); + policyService.deletePolicy(policyId); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/analyze-impact") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD')") + @Operation( + summary = "Analyze policy impact before implementation", + description = "Provides comprehensive analysis of potential policy impacts including affected assets, performance estimates, cost implications, and compliance analysis. Supports FULL, QUICK, and TARGETED analysis types." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Policy impact analysis completed successfully" + ), + @ApiResponse( + responseCode = "400", + description = "Invalid analysis request parameters" + ), + @ApiResponse( + responseCode = "403", + description = "Insufficient permissions to perform policy analysis" + ) + }) + public ResponseEntity analyzePolicyImpact( + @Parameter(description = "Policy impact analysis request with rules content and analysis parameters") + @Valid @RequestBody PolicyImpactRequestDTO request) { + log.info("REST request to analyze policy impact with analysis type: {}", request.getAnalysisType()); + + PolicyImpactResponseDTO response = policyService.analyzePolicy(request); + + log.info("Policy impact analysis completed for analysis ID: {}", response.getAnalysisId()); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/web/rest/PolicyDraftController.java b/src/main/java/com/dalab/policyengine/web/rest/PolicyDraftController.java new file mode 100644 index 0000000000000000000000000000000000000000..eeb8a24737c9038f8a7ef6a7ad593fc97f180778 --- /dev/null +++ b/src/main/java/com/dalab/policyengine/web/rest/PolicyDraftController.java @@ -0,0 +1,421 @@ +package com.dalab.policyengine.web.rest; + +import java.util.List; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.dalab.common.security.SecurityUtils; +import com.dalab.policyengine.dto.PolicyDraftActionDTO; +import com.dalab.policyengine.dto.PolicyDraftInputDTO; +import com.dalab.policyengine.dto.PolicyDraftOutputDTO; +import com.dalab.policyengine.dto.PolicyOutputDTO; +import com.dalab.policyengine.model.PolicyDraftStatus; +import com.dalab.policyengine.service.IPolicyService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +/** + * REST controller for policy draft management operations. + * Provides comprehensive draft workflow including creation, review, approval, and publication. + */ +@RestController +@RequestMapping("/api/v1/policyengine/drafts") +@Tag(name = "Policy Draft Management", description = "Endpoints for managing policy drafts and approval workflows") +public class PolicyDraftController { + + private static final Logger log = LoggerFactory.getLogger(PolicyDraftController.class); + + private final IPolicyService policyService; + + @Autowired + public PolicyDraftController(IPolicyService policyService) { + this.policyService = policyService; + } + + @PostMapping + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD')") + @Operation( + summary = "Create a new policy draft", + description = "Creates a new policy draft that can be edited, submitted for review, and eventually published as an active policy." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Policy draft created successfully"), + @ApiResponse(responseCode = "400", description = "Invalid draft data provided"), + @ApiResponse(responseCode = "409", description = "Draft with this name already exists"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to create drafts") + }) + public ResponseEntity createDraft( + @Parameter(description = "Policy draft creation data") + @Valid @RequestBody PolicyDraftInputDTO draftInput) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Creating policy draft: {} by user: {}", draftInput.getName(), currentUserId); + + PolicyDraftOutputDTO createdDraft = policyService.createDraft(draftInput, currentUserId); + + return ResponseEntity.status(HttpStatus.CREATED).body(createdDraft); + } + + @GetMapping + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD', 'ROLE_USER')") + @Operation( + summary = "Get all policy drafts with filtering", + description = "Retrieves paginated list of policy drafts with optional filtering by status, category, priority, creator, and name search." + ) + public ResponseEntity> getAllDrafts( + @PageableDefault(size = 20, sort = "updatedAt", direction = Sort.Direction.DESC) Pageable pageable, + @Parameter(description = "Filter by draft status") @RequestParam(required = false) PolicyDraftStatus status, + @Parameter(description = "Filter by category") @RequestParam(required = false) String category, + @Parameter(description = "Filter by priority level") @RequestParam(required = false) String priority, + @Parameter(description = "Filter by creator user ID") @RequestParam(required = false) UUID createdBy, + @Parameter(description = "Search in draft names") @RequestParam(required = false) String nameContains) { + + log.debug("Retrieving policy drafts with filters - status: {}, category: {}, priority: {}", + status, category, priority); + + Page drafts = policyService.getAllDrafts( + pageable, status, category, priority, createdBy, nameContains); + + return ResponseEntity.ok(drafts); + } + + @GetMapping("/{draftId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD', 'ROLE_USER')") + @Operation( + summary = "Get policy draft by ID", + description = "Retrieves a specific policy draft with complete workflow information and available actions." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Policy draft retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to view draft") + }) + public ResponseEntity getDraftById( + @Parameter(description = "Draft ID") @PathVariable UUID draftId) { + + log.debug("Retrieving policy draft with ID: {}", draftId); + + PolicyDraftOutputDTO draft = policyService.getDraftById(draftId); + + return ResponseEntity.ok(draft); + } + + @PutMapping("/{draftId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD')") + @Operation( + summary = "Update policy draft", + description = "Updates an existing policy draft. Only allowed for drafts in CREATED or REQUIRES_CHANGES status." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Policy draft updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid draft data provided"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "409", description = "Draft cannot be updated in current status or name conflict"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to update draft") + }) + public ResponseEntity updateDraft( + @Parameter(description = "Draft ID") @PathVariable UUID draftId, + @Parameter(description = "Updated draft data") @Valid @RequestBody PolicyDraftInputDTO draftInput) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Updating policy draft: {} by user: {}", draftId, currentUserId); + + PolicyDraftOutputDTO updatedDraft = policyService.updateDraft(draftId, draftInput, currentUserId); + + return ResponseEntity.ok(updatedDraft); + } + + @DeleteMapping("/{draftId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Delete policy draft", + description = "Deletes a policy draft. Only allowed for drafts in CREATED status." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Policy draft deleted successfully"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "409", description = "Draft cannot be deleted in current status"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to delete draft") + }) + public ResponseEntity deleteDraft( + @Parameter(description = "Draft ID") @PathVariable UUID draftId) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Deleting policy draft: {} by user: {}", draftId, currentUserId); + + policyService.deleteDraft(draftId, currentUserId); + + return ResponseEntity.noContent().build(); + } + + // ========== WORKFLOW ACTION ENDPOINTS ========== + + @PostMapping("/{draftId}/submit") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD')") + @Operation( + summary = "Submit draft for review", + description = "Submits a policy draft for review, transitioning it from CREATED or REQUIRES_CHANGES to SUBMITTED status." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Draft submitted successfully"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "409", description = "Draft cannot be submitted in current status"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to submit draft") + }) + public ResponseEntity submitDraft( + @Parameter(description = "Draft ID") @PathVariable UUID draftId, + @Parameter(description = "Submission action with optional comment") @Valid @RequestBody PolicyDraftActionDTO action) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Submitting policy draft: {} by user: {}", draftId, currentUserId); + + PolicyDraftOutputDTO submittedDraft = policyService.submitDraft(draftId, action, currentUserId); + + return ResponseEntity.ok(submittedDraft); + } + + @PostMapping("/{draftId}/approve") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Approve policy draft", + description = "Approves a policy draft, transitioning it to APPROVED status and making it ready for publication." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Draft approved successfully"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "409", description = "Draft cannot be approved in current status"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to approve draft") + }) + public ResponseEntity approveDraft( + @Parameter(description = "Draft ID") @PathVariable UUID draftId, + @Parameter(description = "Approval action with optional comment") @Valid @RequestBody PolicyDraftActionDTO action) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Approving policy draft: {} by user: {}", draftId, currentUserId); + + PolicyDraftOutputDTO approvedDraft = policyService.approveDraft(draftId, action, currentUserId); + + return ResponseEntity.ok(approvedDraft); + } + + @PostMapping("/{draftId}/reject") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Reject policy draft", + description = "Rejects a policy draft, transitioning it to REJECTED status. A comment explaining the rejection is required." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Draft rejected successfully"), + @ApiResponse(responseCode = "400", description = "Comment is required for rejection"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "409", description = "Draft cannot be rejected in current status"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to reject draft") + }) + public ResponseEntity rejectDraft( + @Parameter(description = "Draft ID") @PathVariable UUID draftId, + @Parameter(description = "Rejection action with required comment") @Valid @RequestBody PolicyDraftActionDTO action) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Rejecting policy draft: {} by user: {}", draftId, currentUserId); + + PolicyDraftOutputDTO rejectedDraft = policyService.rejectDraft(draftId, action, currentUserId); + + return ResponseEntity.ok(rejectedDraft); + } + + @PostMapping("/{draftId}/request-changes") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Request changes to draft", + description = "Requests changes to a policy draft, transitioning it to REQUIRES_CHANGES status. A comment explaining required changes is mandatory." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Changes requested successfully"), + @ApiResponse(responseCode = "400", description = "Comment is required for change request"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "409", description = "Changes cannot be requested for draft in current status"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to request changes") + }) + public ResponseEntity requestChanges( + @Parameter(description = "Draft ID") @PathVariable UUID draftId, + @Parameter(description = "Change request action with required comment") @Valid @RequestBody PolicyDraftActionDTO action) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Requesting changes for policy draft: {} by user: {}", draftId, currentUserId); + + PolicyDraftOutputDTO updatedDraft = policyService.requestChanges(draftId, action, currentUserId); + + return ResponseEntity.ok(updatedDraft); + } + + @PostMapping("/{draftId}/publish") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Publish approved draft as policy", + description = "Publishes an approved policy draft as an active policy. Only APPROVED drafts can be published." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Draft published successfully"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "409", description = "Only approved drafts can be published"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to publish draft") + }) + public ResponseEntity publishDraft( + @Parameter(description = "Draft ID") @PathVariable UUID draftId, + @Parameter(description = "Publication action with optional metadata") @Valid @RequestBody PolicyDraftActionDTO action) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.info("Publishing policy draft: {} by user: {}", draftId, currentUserId); + + PolicyOutputDTO publishedPolicy = policyService.publishDraft(draftId, action, currentUserId); + + return ResponseEntity.ok(publishedPolicy); + } + + @PostMapping("/{draftId}/comments") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD')") + @Operation( + summary = "Add review comment to draft", + description = "Adds a review comment to a policy draft for collaboration and feedback purposes." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Comment added successfully"), + @ApiResponse(responseCode = "400", description = "Invalid comment data"), + @ApiResponse(responseCode = "404", description = "Policy draft not found"), + @ApiResponse(responseCode = "403", description = "Insufficient permissions to comment on draft") + }) + public ResponseEntity addComment( + @Parameter(description = "Draft ID") @PathVariable UUID draftId, + @Parameter(description = "Review comment") @RequestParam String comment) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + String userRole = "REVIEWER"; // Simplified - would get actual roles from security context + + log.debug("Adding comment to policy draft: {} by user: {}", draftId, currentUserId); + + PolicyDraftOutputDTO updatedDraft = policyService.addReviewComment(draftId, comment, currentUserId, userRole); + + return ResponseEntity.ok(updatedDraft); + } + + // ========== SPECIALIZED QUERY ENDPOINTS ========== + + @GetMapping("/my-drafts") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD', 'ROLE_USER')") + @Operation( + summary = "Get drafts requiring my attention", + description = "Retrieves drafts that require attention from the current user (created by them or they are a stakeholder)." + ) + public ResponseEntity> getMyDrafts( + @PageableDefault(size = 20, sort = "updatedAt", direction = Sort.Direction.DESC) Pageable pageable) { + + UUID currentUserId = SecurityUtils.getAuthenticatedUserId(); + log.debug("Retrieving drafts requiring attention for user: {}", currentUserId); + + Page drafts = policyService.getDraftsRequiringAttention(currentUserId, pageable); + + return ResponseEntity.ok(drafts); + } + + @GetMapping("/pending-review") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Get drafts pending review", + description = "Retrieves all drafts that are currently pending review (SUBMITTED or UNDER_REVIEW status)." + ) + public ResponseEntity> getDraftsPendingReview( + @PageableDefault(size = 20, sort = "submittedAt", direction = Sort.Direction.ASC) Pageable pageable) { + + log.debug("Retrieving drafts pending review"); + + Page drafts = policyService.getDraftsPendingReview(pageable); + + return ResponseEntity.ok(drafts); + } + + @GetMapping("/overdue") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Get overdue drafts", + description = "Retrieves drafts that have passed their target implementation date but haven't been published yet." + ) + public ResponseEntity> getOverdueDrafts() { + + log.debug("Retrieving overdue drafts"); + + List overdueDrafts = policyService.getOverdueDrafts(); + + return ResponseEntity.ok(overdueDrafts); + } + + // ========== METADATA ENDPOINTS ========== + + @GetMapping("/categories") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD', 'ROLE_USER')") + @Operation( + summary = "Get available draft categories", + description = "Retrieves list of all categories used in policy drafts for filtering and organization." + ) + public ResponseEntity> getDraftCategories() { + + log.debug("Retrieving draft categories"); + + List categories = policyService.getDraftCategories(); + + return ResponseEntity.ok(categories); + } + + @GetMapping("/tags") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_DATA_STEWARD', 'ROLE_USER')") + @Operation( + summary = "Get available draft tags", + description = "Retrieves list of all tags used in policy drafts for filtering and organization." + ) + public ResponseEntity> getDraftTags() { + + log.debug("Retrieving draft tags"); + + List tags = policyService.getDraftTags(); + + return ResponseEntity.ok(tags); + } + + @GetMapping("/statistics") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER')") + @Operation( + summary = "Get draft statistics", + description = "Retrieves statistical information about policy drafts grouped by status for dashboard displays." + ) + public ResponseEntity> getDraftStatistics() { + + log.debug("Retrieving draft statistics"); + + List statistics = policyService.getDraftStatistics(); + + return ResponseEntity.ok(statistics); + } +} \ No newline at end of file diff --git a/src/main/java/com/dalab/policyengine/web/rest/PolicyEvaluationController.java b/src/main/java/com/dalab/policyengine/web/rest/PolicyEvaluationController.java new file mode 100644 index 0000000000000000000000000000000000000000..f1772219efbaecf97adf2cd21f1bb5758bdfc5be --- /dev/null +++ b/src/main/java/com/dalab/policyengine/web/rest/PolicyEvaluationController.java @@ -0,0 +1,81 @@ +package com.dalab.policyengine.web.rest; + +import java.net.URI; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import com.dalab.common.security.SecurityUtils; +import com.dalab.policyengine.dto.PolicyEvaluationOutputDTO; +import com.dalab.policyengine.dto.PolicyEvaluationRequestDTO; +import com.dalab.policyengine.dto.PolicyEvaluationSummaryDTO; +import com.dalab.policyengine.service.IPolicyEvaluationService; + +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/v1/policyengine") +public class PolicyEvaluationController { + + private static final Logger log = LoggerFactory.getLogger(PolicyEvaluationController.class); + + private final IPolicyEvaluationService policyEvaluationService; + + @Autowired + public PolicyEvaluationController(IPolicyEvaluationService policyEvaluationService) { + this.policyEvaluationService = policyEvaluationService; + } + + @PostMapping("/{policyId}/evaluations") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_SYSTEM')") + public ResponseEntity triggerPolicyEvaluation( + @PathVariable UUID policyId, + @Valid @RequestBody PolicyEvaluationRequestDTO evaluationRequest) { + log.info("REST request to trigger evaluation for Policy: {}", policyId); + + UUID triggeredByUserId = SecurityUtils.getAuthenticatedUserId(); // Can be null if system-triggered via Kafka + PolicyEvaluationOutputDTO evaluation = policyEvaluationService.triggerPolicyEvaluation(policyId, evaluationRequest, triggeredByUserId); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{evaluationId}") + .buildAndExpand(evaluation.getId()) + .toUri(); + + return ResponseEntity.created(location).body(evaluation); + } + + @GetMapping("/evaluations") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_AUDITOR')") + public ResponseEntity> getPolicyEvaluations( + @PageableDefault(size = 50, sort = "evaluatedAt") Pageable pageable, + @RequestParam(required = false) UUID policyId, + @RequestParam(required = false) String targetAssetId, + @RequestParam(required = false) String status) { + log.info("REST request to get Policy Evaluations with filters: policyId={}, targetAssetId={}, status={}", policyId, targetAssetId, status); + Page resultPage = policyEvaluationService.getPolicyEvaluations(pageable, policyId, targetAssetId, status); + return ResponseEntity.ok(resultPage); + } + + @GetMapping("/evaluations/{evaluationId}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_POLICY_MANAGER', 'ROLE_AUDITOR')") + public ResponseEntity getPolicyEvaluationById(@PathVariable UUID evaluationId) { + log.info("REST request to get Policy Evaluation by id: {}", evaluationId); + PolicyEvaluationOutputDTO evaluationDTO = policyEvaluationService.getPolicyEvaluationById(evaluationId); + return ResponseEntity.ok(evaluationDTO); + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dalab/policyengine/DaPolicyEngineApplication.kt b/src/main/kotlin/com/dalab/policyengine/DaPolicyEngineApplication.kt new file mode 100644 index 0000000000000000000000000000000000000000..e56d605c7ea8f0473bdc03c7dfe82b12c1384583 --- /dev/null +++ b/src/main/kotlin/com/dalab/policyengine/DaPolicyEngineApplication.kt @@ -0,0 +1,11 @@ +package com.dalab.policyengine; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.runApplication; + +@SpringBootApplication +class DaPolicyEngineApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..28dfbb0be9f5d661181085f53d1a7f23c5aba478 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,116 @@ +# DALab Policy Engine Service Configuration +# Updated to use the DALab infrastructure + +spring.application.name=da-policyengine + +# Server Configuration +server.port=8080 +server.servlet.context-path=/api/v1/policyengine + +# Database Configuration - using infrastructure PostgreSQL +spring.datasource.url=jdbc:postgresql://localhost:5432/da_policyengine +spring.datasource.username=da_policyengine_user +spring.datasource.password=da_policyengine_pass +spring.datasource.driver-class-name=org.postgresql.Driver + +# Connection Pool Configuration +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.minimum-idle=2 +spring.datasource.hikari.connection-timeout=30000 +spring.datasource.hikari.idle-timeout=600000 +spring.datasource.hikari.max-lifetime=1800000 + +# JPA Configuration +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.default_schema=da_policyengine_schema +spring.jpa.properties.hibernate.jdbc.time_zone=UTC +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.show-sql=false + +# Kafka Configuration - using infrastructure Kafka +spring.kafka.bootstrap-servers=localhost:9092 +spring.kafka.consumer.group-id=${spring.application.name} +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.properties.spring.json.trusted.packages=com.dalab.* +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.retries=3 +spring.kafka.producer.acks=all + +# Security Configuration - using infrastructure Keycloak +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/dalab +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8180/realms/dalab/protocol/openid-connect/certs + +# Management/Actuator Configuration +management.endpoints.web.exposure.include=health,info,metrics,prometheus +management.endpoint.health.show-details=when-authorized +management.metrics.export.prometheus.enabled=true + +# Logging Configuration +logging.level.com.dalab=DEBUG +logging.level.org.springframework.kafka=INFO +logging.level.org.springframework.security=INFO +logging.level.org.hibernate.SQL=DEBUG +logging.pattern.console=%d{HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] - %msg%n + +# DALab Specific Configuration +dalab.service.name=Policy Engine Service +dalab.service.version=1.0.0 +dalab.service.description=Policy evaluation and enforcement engine + +# Common entities database connection (for da-protos entities) +dalab.common-db.url=jdbc:postgresql://localhost:5432/dalab_common +dalab.common-db.username=da_policyengine_user +dalab.common-db.password=da_policyengine_pass + +# Kafka topics +dalab.kafka.topics.asset-changes=dalab.assets.changes +dalab.kafka.topics.policy-actions=dalab.policies.actions +dalab.kafka.topics.policy-evaluations=dalab.policies.evaluations + +# Custom application properties for Kafka topics +app.kafka.topic.asset-change-event=dalab.assets.changes +app.kafka.topic.policy-action-event=dalab.policies.actions + +# Easy Rules Configuration +easy.rules.skipOnFirstAppliedRule=false +easy.rules.skipOnFirstFailedRule=false +easy.rules.rulePriorityThreshold=1 + +# CORS Configuration +dalab.security.allowed-origins=http://localhost:3000,http://localhost:4200 + +# JWT Claims Configuration +dalab.security.jwt.claims.user-id=sub +dalab.security.jwt.claims.username=preferred_username +dalab.security.jwt.claims.roles=realm_access.roles + +# OpenAPI Documentation +springdoc.api-docs.path=/api/v1/policyengine/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.swagger-ui.operationsSorter=alpha +springdoc.swagger-ui.tagsSorter=alpha +springdoc.show-actuator=true + +# Event Center Configuration +dalab.event-center.enabled=true +dalab.event-center.max-events-per-stream=100 +dalab.event-center.analytics-cache-duration=300 +dalab.event-center.notification-retry-attempts=3 +dalab.event-center.notification-timeout-ms=5000 + +# Event Center Kafka Topics +dalab.kafka.topics.event-stream=dalab.events.stream +dalab.kafka.topics.event-notifications=dalab.events.notifications +dalab.kafka.topics.event-analytics=dalab.events.analytics + +# WebSocket Configuration for Event Streaming +spring.websocket.enabled=true +dalab.websocket.event-stream-endpoint=/ws/events +dalab.websocket.allowed-origins=http://localhost:3000,http://localhost:4200 + +# Event Center Source Services +dalab.event-center.source-services=da-catalog,da-discovery,da-compliance,da-policyengine,da-reporting,da-governance,da-lifecycle,da-classification,da-lineage \ No newline at end of file diff --git a/src/test/java/com/dalab/policyengine/controller/PolicyControllerTest.java b/src/test/java/com/dalab/policyengine/controller/PolicyControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0bb56a925a0ed69a1890ad816bf2ddaef7105c88 --- /dev/null +++ b/src/test/java/com/dalab/policyengine/controller/PolicyControllerTest.java @@ -0,0 +1,496 @@ +package com.dalab.policyengine.controller; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDateTime; +import java.util.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import com.dalab.policyengine.dto.*; +import com.dalab.policyengine.model.Policy; +import com.dalab.policyengine.model.PolicyDraft; +import com.dalab.policyengine.model.PolicyStatus; +import com.dalab.policyengine.service.IPolicyService; +import com.dalab.policyengine.service.IPolicyEvaluationService; +import com.dalab.policyengine.service.IPolicyDraftService; +import com.dalab.policyengine.service.IEventSubscriptionService; +import com.fasterxml.jackson.databind.ObjectMapper; + +@WebMvcTest(PolicyController.class) +@ExtendWith(MockitoExtension.class) +@ActiveProfiles("test") +class PolicyControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private IPolicyService policyService; + + @MockBean + private IPolicyEvaluationService policyEvaluationService; + + @MockBean + private IPolicyDraftService policyDraftService; + + @MockBean + private IEventSubscriptionService eventSubscriptionService; + + @Autowired + private ObjectMapper objectMapper; + + private Policy testPolicy; + private PolicyDTO testPolicyDTO; + private PolicyDraft testPolicyDraft; + private PolicyDraftDTO testPolicyDraftDTO; + private PolicyEvaluationRequestDTO testEvaluationRequest; + private PolicyImpactRequestDTO testImpactRequest; + private PolicyImpactResponseDTO testImpactResponse; + + @BeforeEach + void setUp() { + // Create test policy + testPolicy = new Policy(); + testPolicy.setId(UUID.randomUUID()); + testPolicy.setName("Test Policy"); + testPolicy.setDescription("Test policy for compliance"); + testPolicy.setRulesContent("when asset.type == 'PII' then tag('SENSITIVE')"); + testPolicy.setStatus(PolicyStatus.ENABLED); + testPolicy.setCreatedAt(LocalDateTime.now()); + testPolicy.setUpdatedAt(LocalDateTime.now()); + + // Create test policy DTO + testPolicyDTO = PolicyDTO.builder() + .id(testPolicy.getId()) + .name("Test Policy") + .description("Test policy for compliance") + .rulesContent("when asset.type == 'PII' then tag('SENSITIVE')") + .status("ENABLED") + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + // Create test policy draft + testPolicyDraft = new PolicyDraft(); + testPolicyDraft.setId(UUID.randomUUID()); + testPolicyDraft.setName("Test Draft Policy"); + testPolicyDraft.setDescription("Draft policy for testing"); + testPolicyDraft.setRulesContent("when asset.type == 'PHI' then tag('HEALTHCARE')"); + testPolicyDraft.setStatus("DRAFT"); + testPolicyDraft.setCreatedAt(LocalDateTime.now()); + + // Create test policy draft DTO + testPolicyDraftDTO = PolicyDraftDTO.builder() + .id(testPolicyDraft.getId()) + .name("Test Draft Policy") + .description("Draft policy for testing") + .rulesContent("when asset.type == 'PHI' then tag('HEALTHCARE')") + .status("DRAFT") + .createdAt(LocalDateTime.now()) + .build(); + + // Create test evaluation request + testEvaluationRequest = PolicyEvaluationRequestDTO.builder() + .policyId(testPolicy.getId()) + .assetId(UUID.randomUUID()) + .evaluationContext(Map.of("asset.type", "PII")) + .build(); + + // Create test impact request + testImpactRequest = PolicyImpactRequestDTO.builder() + .rulesContent("when asset.type == 'PII' then tag('SENSITIVE')") + .analysisType("FULL") + .includePerformanceEstimate(true) + .includeCostImpact(true) + .includeComplianceImpact(true) + .build(); + + // Create test impact response + testImpactResponse = PolicyImpactResponseDTO.builder() + .analysisId("impact-12345678") + .analysisType("FULL") + .build(); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void createPolicy_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + PolicyCreateRequestDTO request = PolicyCreateRequestDTO.builder() + .name("New Test Policy") + .description("New test policy for compliance") + .rulesContent("when asset.type == 'PII' then tag('SENSITIVE')") + .build(); + + when(policyService.createPolicy(any(PolicyCreateRequestDTO.class))).thenReturn(testPolicyDTO); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(testPolicy.getId().toString())) + .andExpect(jsonPath("$.name").value("Test Policy")) + .andExpect(jsonPath("$.status").value("ENABLED")); + } + + @Test + @WithMockUser(authorities = "ROLE_USER") + void createPolicy_AsUser_ShouldBeForbidden() throws Exception { + // Given + PolicyCreateRequestDTO request = PolicyCreateRequestDTO.builder() + .name("New Test Policy") + .description("New test policy for compliance") + .rulesContent("when asset.type == 'PII' then tag('SENSITIVE')") + .build(); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void getAllPolicies_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + List policyList = Arrays.asList(testPolicyDTO); + Page policyPage = new PageImpl<>(policyList, PageRequest.of(0, 20), 1); + + when(policyService.getAllPolicies(any(Pageable.class))).thenReturn(policyPage); + + // When & Then + mockMvc.perform(get("/api/v1/policyengine/policies") + .param("page", "0") + .param("size", "20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].id").value(testPolicy.getId().toString())) + .andExpect(jsonPath("$.content[0].name").value("Test Policy")) + .andExpect(jsonPath("$.totalElements").value(1)); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void getPolicyById_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + when(policyService.getPolicyById(testPolicy.getId())).thenReturn(Optional.of(testPolicyDTO)); + + // When & Then + mockMvc.perform(get("/api/v1/policyengine/policies/{policyId}", testPolicy.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(testPolicy.getId().toString())) + .andExpect(jsonPath("$.name").value("Test Policy")) + .andExpect(jsonPath("$.status").value("ENABLED")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void getPolicyById_PolicyNotFound_ShouldReturnNotFound() throws Exception { + // Given + UUID nonExistentId = UUID.randomUUID(); + when(policyService.getPolicyById(nonExistentId)).thenReturn(Optional.empty()); + + // When & Then + mockMvc.perform(get("/api/v1/policyengine/policies/{policyId}", nonExistentId)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void updatePolicy_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + PolicyUpdateRequestDTO request = PolicyUpdateRequestDTO.builder() + .name("Updated Test Policy") + .description("Updated test policy for compliance") + .rulesContent("when asset.type == 'PII' then tag('SENSITIVE') and notify('admin')") + .status("ENABLED") + .build(); + + PolicyDTO updatedPolicy = PolicyDTO.builder() + .id(testPolicy.getId()) + .name("Updated Test Policy") + .description("Updated test policy for compliance") + .rulesContent("when asset.type == 'PII' then tag('SENSITIVE') and notify('admin')") + .status("ENABLED") + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + when(policyService.updatePolicy(eq(testPolicy.getId()), any(PolicyUpdateRequestDTO.class))) + .thenReturn(updatedPolicy); + + // When & Then + mockMvc.perform(put("/api/v1/policyengine/policies/{policyId}", testPolicy.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(testPolicy.getId().toString())) + .andExpect(jsonPath("$.name").value("Updated Test Policy")) + .andExpect(jsonPath("$.status").value("ENABLED")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void deletePolicy_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + doNothing().when(policyService).deletePolicy(testPolicy.getId()); + + // When & Then + mockMvc.perform(delete("/api/v1/policyengine/policies/{policyId}", testPolicy.getId()) + .with(csrf())) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(authorities = "ROLE_USER") + void deletePolicy_AsUser_ShouldBeForbidden() throws Exception { + // When & Then + mockMvc.perform(delete("/api/v1/policyengine/policies/{policyId}", testPolicy.getId()) + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void evaluatePolicy_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + PolicyEvaluationResponseDTO evaluationResponse = PolicyEvaluationResponseDTO.builder() + .policyId(testPolicy.getId()) + .assetId(testEvaluationRequest.getAssetId()) + .evaluationResult("PASS") + .actions(Arrays.asList("tag('SENSITIVE')")) + .evaluationTimestamp(LocalDateTime.now()) + .build(); + + when(policyEvaluationService.evaluatePolicy(any(PolicyEvaluationRequestDTO.class))) + .thenReturn(evaluationResponse); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies/evaluate") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testEvaluationRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.policyId").value(testPolicy.getId().toString())) + .andExpect(jsonPath("$.evaluationResult").value("PASS")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void analyzePolicyImpact_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + when(policyService.analyzePolicy(any(PolicyImpactRequestDTO.class))) + .thenReturn(testImpactResponse); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies/{policyId}/impact-preview", testPolicy.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testImpactRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.analysisId").value("impact-12345678")) + .andExpect(jsonPath("$.analysisType").value("FULL")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void createPolicyDraft_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + PolicyDraftCreateRequestDTO request = PolicyDraftCreateRequestDTO.builder() + .name("New Draft Policy") + .description("New draft policy for testing") + .rulesContent("when asset.type == 'PHI' then tag('HEALTHCARE')") + .build(); + + when(policyDraftService.createPolicyDraft(any(PolicyDraftCreateRequestDTO.class))) + .thenReturn(testPolicyDraftDTO); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies/drafts") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(testPolicyDraft.getId().toString())) + .andExpect(jsonPath("$.name").value("Test Draft Policy")) + .andExpect(jsonPath("$.status").value("DRAFT")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void getAllPolicyDrafts_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + List draftList = Arrays.asList(testPolicyDraftDTO); + Page draftPage = new PageImpl<>(draftList, PageRequest.of(0, 20), 1); + + when(policyDraftService.getAllPolicyDrafts(any(Pageable.class))).thenReturn(draftPage); + + // When & Then + mockMvc.perform(get("/api/v1/policyengine/policies/drafts") + .param("page", "0") + .param("size", "20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].id").value(testPolicyDraft.getId().toString())) + .andExpect(jsonPath("$.content[0].name").value("Test Draft Policy")) + .andExpect(jsonPath("$.totalElements").value(1)); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void getPolicyDraftById_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + when(policyDraftService.getPolicyDraftById(testPolicyDraft.getId())) + .thenReturn(Optional.of(testPolicyDraftDTO)); + + // When & Then + mockMvc.perform(get("/api/v1/policyengine/policies/drafts/{draftId}", testPolicyDraft.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(testPolicyDraft.getId().toString())) + .andExpect(jsonPath("$.name").value("Test Draft Policy")) + .andExpect(jsonPath("$.status").value("DRAFT")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void updatePolicyDraft_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + PolicyDraftUpdateRequestDTO request = PolicyDraftUpdateRequestDTO.builder() + .name("Updated Draft Policy") + .description("Updated draft policy for testing") + .rulesContent("when asset.type == 'PHI' then tag('HEALTHCARE') and encrypt()") + .build(); + + PolicyDraftDTO updatedDraft = PolicyDraftDTO.builder() + .id(testPolicyDraft.getId()) + .name("Updated Draft Policy") + .description("Updated draft policy for testing") + .rulesContent("when asset.type == 'PHI' then tag('HEALTHCARE') and encrypt()") + .status("DRAFT") + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + when(policyDraftService.updatePolicyDraft(eq(testPolicyDraft.getId()), any(PolicyDraftUpdateRequestDTO.class))) + .thenReturn(updatedDraft); + + // When & Then + mockMvc.perform(put("/api/v1/policyengine/policies/drafts/{draftId}", testPolicyDraft.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(testPolicyDraft.getId().toString())) + .andExpect(jsonPath("$.name").value("Updated Draft Policy")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void approvePolicyDraft_AsDataEngineer_ShouldSucceed() throws Exception { + // Given + PolicyDraftApprovalRequestDTO request = PolicyDraftApprovalRequestDTO.builder() + .approved(true) + .comments("Approved for production use") + .build(); + + PolicyDTO approvedPolicy = PolicyDTO.builder() + .id(UUID.randomUUID()) + .name("Approved Policy") + .description("Policy approved from draft") + .rulesContent("when asset.type == 'PHI' then tag('HEALTHCARE')") + .status("ENABLED") + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + when(policyDraftService.approvePolicyDraft(eq(testPolicyDraft.getId()), any(PolicyDraftApprovalRequestDTO.class))) + .thenReturn(approvedPolicy); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies/drafts/{draftId}/approve", testPolicyDraft.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Approved Policy")) + .andExpect(jsonPath("$.status").value("ENABLED")); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void createPolicy_InvalidRequest_ShouldReturnBadRequest() throws Exception { + // Given + PolicyCreateRequestDTO request = PolicyCreateRequestDTO.builder() + .name("") // Invalid: empty name + .description("Test policy") + .rulesContent("invalid rule syntax") + .build(); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void evaluatePolicy_InvalidRequest_ShouldReturnBadRequest() throws Exception { + // Given + PolicyEvaluationRequestDTO request = PolicyEvaluationRequestDTO.builder() + .policyId(null) // Invalid: null policy ID + .assetId(UUID.randomUUID()) + .evaluationContext(Map.of("asset.type", "PII")) + .build(); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies/evaluate") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(authorities = "ROLE_DATA_ENGINEER") + void analyzePolicyImpact_InvalidRequest_ShouldReturnBadRequest() throws Exception { + // Given + PolicyImpactRequestDTO request = PolicyImpactRequestDTO.builder() + .rulesContent("") // Invalid: empty rules content + .analysisType("INVALID_TYPE") // Invalid: unknown analysis type + .build(); + + // When & Then + mockMvc.perform(post("/api/v1/policyengine/policies/{policyId}/impact-preview", testPolicy.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..6de2b292965f2da04ca806fef2b0c5cb87cd9c6c --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,90 @@ +# Test configuration for da-policyengine integration tests +spring: + application: + name: da-policyengine-test + + # Database configuration (using H2 for tests) + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: password + hikari: + auto-commit: false + maximum-pool-size: 10 + minimum-idle: 2 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop + show-sql: false + properties: + hibernate: + format_sql: false + jdbc: + lob: + non_contextual_creation: true + dialect: org.hibernate.dialect.H2Dialect + + h2: + console: + enabled: false + + # Disable Liquibase for tests + liquibase: + enabled: false + + # Disable Docker for tests + docker: + compose: + enabled: false + + # Task execution configuration for tests + task: + execution: + pool: + core-size: 2 + max-size: 4 + queue-capacity: 100 + thread-name-prefix: test-task- + + # Cache configuration for tests + cache: + type: simple + +# Disable Docker for tests +testcontainers: + enabled: false + +# Security configuration for tests +dalab: + security: + jwt: + enabled: false + oauth2: + enabled: false + +# Kafka configuration for tests (disabled) +spring: + kafka: + enabled: false + bootstrap-servers: localhost:9092 + consumer: + group-id: da-policyengine-test + auto-offset-reset: earliest + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer + +# Easy Rules configuration for tests +easy-rules: + rules-engine: + skip-on-first-failed-rule: false + skip-on-first-applied-rule: false + skip-on-first-non-triggered-rule: false + rule-priority-threshold: 2147483647 \ No newline at end of file