Add execution ownership filtering and usage stats
This commit is contained in:
parent
8c26b5d8fc
commit
eb66452f03
|
|
@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
|
@ -33,6 +34,7 @@ import it.cnr.isti.workflow.manager.executions.ExecutionSimulationRequest;
|
|||
import it.cnr.isti.workflow.manager.executions.ExecutionsService;
|
||||
import it.cnr.isti.workflow.manager.executions.api.ExecutionContextView;
|
||||
import it.cnr.isti.workflow.manager.executions.api.ExecutionView;
|
||||
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowEntity;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowRepository;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
|
@ -98,7 +100,8 @@ public class ExecutionsController {
|
|||
*/
|
||||
@Operation(summary = "Creates an execution", description = "Creates a new execution object for the specified flow.")
|
||||
@PostMapping()
|
||||
public ExecutionView create(@RequestBody @NotNull String flowId) {
|
||||
public ExecutionView create(@RequestBody @NotNull String flowId,
|
||||
@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
logger.info("Creating execution for flow {}", flowId);
|
||||
if (flowId == null || flowId.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Flow ID must not be null or empty");
|
||||
|
|
@ -107,7 +110,8 @@ public class ExecutionsController {
|
|||
FlowEntity flow = flowRepository.findById(flowId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Flow with id " + flowId + " not found"));
|
||||
try{
|
||||
ExecutionObject eo = executionService.createExecution(flow.getName(), flow.getFlow());
|
||||
ExecutionObject eo = executionService.createExecution(flow.getName(), flow.getFlow(),
|
||||
userDetails == null ? null : userDetails.getUsername());
|
||||
return ExecutionView.fromExecution(eo);
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
|
|
@ -124,8 +128,8 @@ public class ExecutionsController {
|
|||
*/
|
||||
@Operation(summary = "Retrieves executions", description = "Retrieves a list of all executions")
|
||||
@GetMapping()
|
||||
public List<ExecutionView> getAll() {
|
||||
return executionService.getAllExecutions().stream().map(ExecutionView::fromExecution).toList();
|
||||
public List<ExecutionView> getAll(@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return visibleExecutions(userDetails).stream().map(ExecutionView::fromExecution).toList();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -137,8 +141,8 @@ public class ExecutionsController {
|
|||
*/
|
||||
@Operation(summary = "Retrieves contexts", description = "Retrieves all execution contexts")
|
||||
@GetMapping(path = "contexts")
|
||||
public Map<String,ExecutionContextView> getAllContexts() {
|
||||
return executionService.getAllExecutions().stream()
|
||||
public Map<String,ExecutionContextView> getAllContexts(@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return visibleExecutions(userDetails).stream()
|
||||
.collect(Collectors.toMap(ExecutionObject::getId, eo -> ExecutionContextView.fromContext(eo.getContext())));
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +155,24 @@ public class ExecutionsController {
|
|||
* @throws IllegalArgumentException if the execution with the specified ID is not found
|
||||
*/
|
||||
@Operation(summary = "Retrieves an execution", description = "Retrieves a specific execution by its ID")
|
||||
public ExecutionView get(@PathVariable String id) {
|
||||
return ExecutionView.fromExecution(executionService.getExecution(id));
|
||||
public ExecutionView get(@PathVariable String id, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(visibleExecution(id, userDetails));
|
||||
}
|
||||
|
||||
private List<ExecutionObject> visibleExecutions(LoginEntity userDetails) {
|
||||
List<ExecutionObject> allExecutions = executionService.getAllExecutions();
|
||||
if (userDetails == null) {
|
||||
return allExecutions;
|
||||
}
|
||||
return allExecutions.stream()
|
||||
.filter(execution -> userDetails.getUsername().equals(execution.getOwner()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping(path = "{id}/events")
|
||||
@Operation(summary = "Retrieves execution events", description = "Retrieves the user-readable event log for a specific execution.")
|
||||
public List<ExecutionEvent> getEvents(@PathVariable String id) {
|
||||
return executionService.getExecution(id).getContext().getEvents();
|
||||
public List<ExecutionEvent> getEvents(@PathVariable String id, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return visibleExecution(id, userDetails).getContext().getEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -169,8 +183,8 @@ public class ExecutionsController {
|
|||
*/
|
||||
@DeleteMapping(path = "{id}")
|
||||
@Operation(summary = "Deletes an execution", description = "Deletes an execution with the specified ID")
|
||||
public ResponseEntity<?> delete(@PathVariable String id) {
|
||||
executionService.removeExecution(id);
|
||||
public ResponseEntity<?> delete(@PathVariable String id, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
executionService.removeExecution(visibleExecution(id, userDetails).getId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
|
@ -182,27 +196,30 @@ public class ExecutionsController {
|
|||
*/
|
||||
@PutMapping(path = "{id}/start")
|
||||
@Operation(summary = "Starts an execution", description = "Starts the execution of a workflow with the given ID")
|
||||
public ExecutionView start(@PathVariable String id) {
|
||||
return ExecutionView.fromExecution(executionService.startExecution(id));
|
||||
public ExecutionView start(@PathVariable String id, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(executionService.startExecution(visibleExecution(id, userDetails).getId()));
|
||||
}
|
||||
|
||||
@PutMapping(path = "{id}/simulate")
|
||||
@Operation(summary = "Starts an execution in simulation mode",
|
||||
description = "Starts the execution and enables simulation for interactive nodes.")
|
||||
public ExecutionView simulate(@PathVariable String id, @RequestBody @jakarta.validation.Valid ExecutionSimulationRequest request) {
|
||||
return ExecutionView.fromExecution(executionService.startSimulationExecution(id, request.simulator()));
|
||||
public ExecutionView simulate(@PathVariable String id,
|
||||
@RequestBody @jakarta.validation.Valid ExecutionSimulationRequest request,
|
||||
@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(
|
||||
executionService.startSimulationExecution(visibleExecution(id, userDetails).getId(), request.simulator()));
|
||||
}
|
||||
|
||||
@PutMapping(path = "{id}/resume")
|
||||
@Operation(summary = "Resumes an execution", description = "Rebuilds a persisted execution and resumes it when possible.")
|
||||
public ExecutionView resume(@PathVariable String id) {
|
||||
return ExecutionView.fromExecution(executionService.resumeExecution(id));
|
||||
public ExecutionView resume(@PathVariable String id, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(executionService.resumeExecution(visibleExecution(id, userDetails).getId()));
|
||||
}
|
||||
|
||||
@PutMapping(path = "{id}/cancel")
|
||||
@Operation(summary = "Cancels an execution", description = "Cancels the execution, clears runtime state and marks it as CANCELLED.")
|
||||
public ExecutionView cancel(@PathVariable String id) {
|
||||
return ExecutionView.fromExecution(executionService.cancelExecution(id));
|
||||
public ExecutionView cancel(@PathVariable String id, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(executionService.cancelExecution(visibleExecution(id, userDetails).getId()));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -219,15 +236,18 @@ public class ExecutionsController {
|
|||
@PutMapping(path = "{executionId}/node/{nodeId}/input/{inputName}/text", consumes = "text/plain")
|
||||
@Operation(summary = "Prepares string inputs", description = "Prepares the input for an execution by associating a given input string with a specific input name and execution ID.")
|
||||
public ExecutionView prepareStringInputs(@PathVariable String executionId, @PathVariable String nodeId, @PathVariable String inputName,
|
||||
@RequestBody String input) {
|
||||
return ExecutionView.fromExecution(executionService.prepareInput(executionId, nodeId, inputName, input));
|
||||
@RequestBody String input, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(
|
||||
executionService.prepareInput(visibleExecution(executionId, userDetails).getId(), nodeId, inputName, input));
|
||||
}
|
||||
|
||||
@PutMapping(path = "{executionId}/node/{nodeId}/input/{inputName}/texts", consumes = "application/json")
|
||||
@Operation(summary = "Prepares string array inputs", description = "Prepares an array of string inputs for an execution node input.")
|
||||
public ExecutionView prepareStringArrayInputs(@PathVariable String executionId, @PathVariable String nodeId,
|
||||
@PathVariable String inputName, @RequestBody List<String> input) {
|
||||
return ExecutionView.fromExecution(executionService.prepareInput(executionId, nodeId, inputName, input));
|
||||
@PathVariable String inputName, @RequestBody List<String> input,
|
||||
@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(
|
||||
executionService.prepareInput(visibleExecution(executionId, userDetails).getId(), nodeId, inputName, input));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -245,11 +265,12 @@ public class ExecutionsController {
|
|||
@PutMapping(path = "{executionId}/node/{nodeId}/input/{inputName}/file", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Prepares file inputs", description = "Prepares file inputs for a specific execution by uploading a file and associating it with the given input name and execution ID.")
|
||||
public ExecutionView prepareFileInputs(@PathVariable String executionId, @PathVariable String nodeId, @PathVariable String inputName,
|
||||
@RequestParam MultipartFile file) {
|
||||
@RequestParam MultipartFile file, @AuthenticationPrincipal LoginEntity userDetails) {
|
||||
try {
|
||||
File myFile = File.createTempFile(inputName, file.getOriginalFilename());
|
||||
file.transferTo(myFile);
|
||||
return ExecutionView.fromExecution(executionService.prepareInput(executionId, nodeId, inputName, myFile));
|
||||
return ExecutionView.fromExecution(
|
||||
executionService.prepareInput(visibleExecution(executionId, userDetails).getId(), nodeId, inputName, myFile));
|
||||
} catch (IOException e) {
|
||||
throw new WebServerException("Error while creating file", e);
|
||||
}
|
||||
|
|
@ -259,7 +280,8 @@ public class ExecutionsController {
|
|||
@PutMapping(path = "{executionId}/node/{nodeId}/input/{inputName}/files", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Prepares file array inputs", description = "Prepares multiple file inputs for a specific execution node input.")
|
||||
public ExecutionView prepareFileArrayInputs(@PathVariable String executionId, @PathVariable String nodeId,
|
||||
@PathVariable String inputName, @RequestParam List<MultipartFile> files) {
|
||||
@PathVariable String inputName, @RequestParam List<MultipartFile> files,
|
||||
@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
List<File> preparedFiles = new ArrayList<>();
|
||||
try {
|
||||
for (MultipartFile file : files) {
|
||||
|
|
@ -267,7 +289,8 @@ public class ExecutionsController {
|
|||
file.transferTo(myFile);
|
||||
preparedFiles.add(myFile);
|
||||
}
|
||||
return ExecutionView.fromExecution(executionService.prepareInput(executionId, nodeId, inputName, preparedFiles));
|
||||
return ExecutionView.fromExecution(
|
||||
executionService.prepareInput(visibleExecution(executionId, userDetails).getId(), nodeId, inputName, preparedFiles));
|
||||
} catch (IOException e) {
|
||||
throw new WebServerException("Error while creating file", e);
|
||||
}
|
||||
|
|
@ -276,16 +299,30 @@ public class ExecutionsController {
|
|||
@PutMapping(path = "{executionId}/node/{nodeId}/interaction/{fieldName}/text", consumes = "text/plain")
|
||||
@Operation(summary = "Provides human interaction output", description = "Provides the value for a waiting human interaction step.")
|
||||
public ExecutionView provideInteractionValue(@PathVariable String executionId, @PathVariable String nodeId,
|
||||
@PathVariable String fieldName, @RequestBody String value) {
|
||||
return ExecutionView.fromExecution(executionService.setInteractionValue(executionId, nodeId, fieldName, value));
|
||||
@PathVariable String fieldName, @RequestBody String value,
|
||||
@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(
|
||||
executionService.setInteractionValue(visibleExecution(executionId, userDetails).getId(), nodeId, fieldName, value));
|
||||
}
|
||||
|
||||
@PutMapping(path = "{executionId}/authorizations")
|
||||
@Operation(summary = "Provides execution authorization", description = "Stores a required provider authorization value for an execution.")
|
||||
public ExecutionView provideAuthorization(@PathVariable String executionId,
|
||||
@RequestBody @jakarta.validation.Valid ExecutionAuthorizationValueRequest request) {
|
||||
return ExecutionView.fromExecution(executionService.setAuthorizationValue(executionId, request.key(), request.value()));
|
||||
@RequestBody @jakarta.validation.Valid ExecutionAuthorizationValueRequest request,
|
||||
@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
return ExecutionView.fromExecution(
|
||||
executionService.setAuthorizationValue(visibleExecution(executionId, userDetails).getId(), request.key(), request.value()));
|
||||
}
|
||||
|
||||
private ExecutionObject visibleExecution(String id, LoginEntity userDetails) {
|
||||
ExecutionObject execution = executionService.getExecution(id);
|
||||
if (userDetails == null) {
|
||||
return execution;
|
||||
}
|
||||
if (!userDetails.getUsername().equals(execution.getOwner())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Execution with id " + id + " is not accessible");
|
||||
}
|
||||
return execution;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
package it.cnr.isti.workflow.manager.controllers;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import it.cnr.isti.workflow.manager.auth.model.UserRole;
|
||||
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
||||
import it.cnr.isti.workflow.manager.stats.UserStatsNotFoundException;
|
||||
import it.cnr.isti.workflow.manager.stats.UserStatsService;
|
||||
import it.cnr.isti.workflow.manager.stats.model.SystemUsageStatsView;
|
||||
import it.cnr.isti.workflow.manager.stats.model.UserUsageStatsView;
|
||||
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@RestController
|
||||
@RequestMapping("/stats")
|
||||
public class StatsController {
|
||||
|
||||
@Autowired
|
||||
private UserStatsService userStatsService;
|
||||
|
||||
@GetMapping()
|
||||
@Operation(summary = "Get system usage stats", description = "Returns aggregated usage statistics for the whole system. Accessible only to admins.")
|
||||
public ResponseEntity<SystemUsageStatsView> getSystemStats(@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
if (userDetails == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
boolean admin = isAdmin(userDetails);
|
||||
if (!admin) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
return ResponseEntity.ok(userStatsService.getSystemStats());
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
@Operation(summary = "Get all user usage stats", description = "Returns aggregated usage statistics for all users. Accessible only to admins.")
|
||||
public ResponseEntity<List<String>> getAllUserStats(@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
if (userDetails == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
boolean admin = isAdmin(userDetails);
|
||||
if (!admin) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
return ResponseEntity.ok(userStatsService.getAllUsersInStats());
|
||||
}
|
||||
|
||||
@GetMapping("/users/{username}")
|
||||
@Operation(summary = "Get user usage stats", description = "Returns aggregated usage statistics for a user. Accessible only to admins.")
|
||||
public ResponseEntity<UserUsageStatsView> getUserStats(@PathVariable String username,
|
||||
@AuthenticationPrincipal LoginEntity userDetails) {
|
||||
if (userDetails == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
boolean admin = isAdmin(userDetails);
|
||||
if (!admin) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
try {
|
||||
return ResponseEntity.ok(userStatsService.getUserStats(username));
|
||||
} catch (UserStatsNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAdmin(LoginEntity userDetails) {
|
||||
if (userDetails != null && userDetails.effectiveRole() == UserRole.ADMIN) {
|
||||
return true;
|
||||
}
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return authentication != null && authentication.getAuthorities().stream()
|
||||
.anyMatch(authority -> "ROLE_ADMIN".equals(authority.getAuthority()));
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,8 @@ public class ExecutionObject {
|
|||
|
||||
String name;
|
||||
|
||||
String owner;
|
||||
|
||||
@JsonIgnore
|
||||
FlowData flow;
|
||||
|
||||
|
|
@ -59,8 +61,10 @@ public class ExecutionObject {
|
|||
LLMDescriptor interactionSimulationDescriptor;
|
||||
|
||||
@Builder
|
||||
public ExecutionObject(String executionName, FlowData flow, List<ExecutionAuthorizationRequirement> requiredAuthorizations) {
|
||||
public ExecutionObject(String executionName, FlowData flow, List<ExecutionAuthorizationRequirement> requiredAuthorizations,
|
||||
String owner) {
|
||||
this.name = executionName;
|
||||
this.owner = owner;
|
||||
this.flow = flow;
|
||||
this.stepConnections = flow.getConnections() == null ? List.of() : List.copyOf(flow.getConnections());
|
||||
this.requiredAuthorizations = requiredAuthorizations == null ? List.of() : List.copyOf(requiredAuthorizations);
|
||||
|
|
|
|||
|
|
@ -50,12 +50,17 @@ public class ExecutionsService {
|
|||
MCPAgentService mcpAgentService;
|
||||
|
||||
public ExecutionObject createExecution(String executionName, FlowData flow) {
|
||||
return createExecution(executionName, flow, null);
|
||||
}
|
||||
|
||||
public ExecutionObject createExecution(String executionName, FlowData flow, String owner) {
|
||||
flowExecutionValidator.validate(flow);
|
||||
List<ExecutionAuthorizationRequirement> requiredAuthorizations = resolveRequiredAuthorizations(flow);
|
||||
ExecutionObject execObject = ExecutionObject.builder()
|
||||
.executionName(executionName)
|
||||
.flow(flow)
|
||||
.requiredAuthorizations(requiredAuthorizations)
|
||||
.owner(owner)
|
||||
.build();
|
||||
attachPersistence(execObject);
|
||||
executions.put(execObject.getId(), execObject);
|
||||
|
|
@ -75,7 +80,7 @@ public class ExecutionsService {
|
|||
flowDataBuilder.connections(flow.getConnections());
|
||||
}
|
||||
FlowData flowData = flowDataBuilder.build();
|
||||
return createExecution(flow.getName(), flowData);
|
||||
return createExecution(flow.getName(), flowData, null);
|
||||
}
|
||||
|
||||
public ExecutionObject getExecution(String id) {
|
||||
|
|
@ -334,6 +339,7 @@ public class ExecutionsService {
|
|||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id(executionObject.getId())
|
||||
.name(executionObject.getName())
|
||||
.owner(executionObject.getOwner())
|
||||
.creationTime(executionObject.getCreationTime())
|
||||
.lastUpdateTime(System.currentTimeMillis())
|
||||
.flow(executionObject.getFlow())
|
||||
|
|
@ -347,6 +353,7 @@ public class ExecutionsService {
|
|||
.executionName(entity.getName())
|
||||
.flow(flow)
|
||||
.requiredAuthorizations(resolveRequiredAuthorizations(flow))
|
||||
.owner(entity.getOwner())
|
||||
.build();
|
||||
ExecutionSnapshot snapshot = entity.getSnapshot();
|
||||
executionObject.restore(entity.getId(), entity.getCreationTime(),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public class ExecutionEntity {
|
|||
@NotBlank
|
||||
private String name;
|
||||
|
||||
private String owner;
|
||||
|
||||
private long creationTime;
|
||||
|
||||
private long lastUpdateTime;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package it.cnr.isti.workflow.manager.stats;
|
||||
|
||||
public class UserStatsNotFoundException extends RuntimeException {
|
||||
public UserStatsNotFoundException(String username) {
|
||||
super("User not found: " + username);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package it.cnr.isti.workflow.manager.stats;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import it.cnr.isti.workflow.manager.auth.repo.AuthRepository;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionStatus;
|
||||
import it.cnr.isti.workflow.manager.executions.persistence.ExecutionSnapshot;
|
||||
import it.cnr.isti.workflow.manager.executions.repo.ExecutionEntity;
|
||||
import it.cnr.isti.workflow.manager.executions.repo.ExecutionRepository;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowEntity;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowRepository;
|
||||
import it.cnr.isti.workflow.manager.stats.model.SystemUsageStatsView;
|
||||
import it.cnr.isti.workflow.manager.stats.model.UserUsageStatsView;
|
||||
|
||||
@Service
|
||||
public class UserStatsService {
|
||||
|
||||
@Autowired
|
||||
private AuthRepository authRepository;
|
||||
|
||||
@Autowired
|
||||
private FlowRepository flowRepository;
|
||||
|
||||
@Autowired
|
||||
private ExecutionRepository executionRepository;
|
||||
|
||||
public SystemUsageStatsView getSystemStats() {
|
||||
List<FlowEntity> flows = flowRepository.findAll();
|
||||
List<ExecutionEntity> executions = executionRepository.findAll();
|
||||
|
||||
long executionsRunning = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.map(this::statusOf)
|
||||
.filter(status -> status != null && status.isRunningState())
|
||||
.count();
|
||||
|
||||
long executionsSucceeded = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.map(this::statusOf)
|
||||
.filter(ExecutionStatus.SUCCESS::equals)
|
||||
.count();
|
||||
|
||||
long executionsFailed = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.map(this::statusOf)
|
||||
.filter(ExecutionStatus.ERROR::equals)
|
||||
.count();
|
||||
|
||||
long simulationsStarted = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.filter(snapshot -> snapshot != null && snapshot.isInteractionSimulationEnabled())
|
||||
.count();
|
||||
|
||||
LocalDateTime lastFlowUpdateAt = flows.stream()
|
||||
.map(FlowEntity::getLastUpdateAt)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.max(Comparator.naturalOrder())
|
||||
.orElse(null);
|
||||
|
||||
Long lastExecutionAt = executions.stream()
|
||||
.map(ExecutionEntity::getLastUpdateTime)
|
||||
.max(Long::compareTo)
|
||||
.orElse(null);
|
||||
|
||||
return SystemUsageStatsView.builder()
|
||||
.usersCount(authRepository.count())
|
||||
.flowsCreated(flows.size())
|
||||
.flowsPublished(flows.stream().filter(FlowEntity::isPublished).count())
|
||||
.flowsFinalized(flows.stream().filter(FlowEntity::isFinalized).count())
|
||||
.executionsCreated(executions.size())
|
||||
.executionsRunning(executionsRunning)
|
||||
.executionsSucceeded(executionsSucceeded)
|
||||
.executionsFailed(executionsFailed)
|
||||
.simulationsStarted(simulationsStarted)
|
||||
.lastFlowUpdateAt(lastFlowUpdateAt)
|
||||
.lastExecutionAt(lastExecutionAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
public List<String> getAllUsersInStats() {
|
||||
return authRepository.findAll().stream()
|
||||
.map(login -> login.getUsername())
|
||||
.distinct()
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public UserUsageStatsView getUserStats(String username) {
|
||||
if (!authRepository.existsById(username)) {
|
||||
throw new UserStatsNotFoundException(username);
|
||||
}
|
||||
|
||||
return buildUserStats(username);
|
||||
}
|
||||
|
||||
private UserUsageStatsView buildUserStats(String username) {
|
||||
List<FlowEntity> flows = flowRepository.findByOwner(username);
|
||||
List<ExecutionEntity> executions = executionRepository.findByOwner(username);
|
||||
|
||||
long executionsRunning = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.map(this::statusOf)
|
||||
.filter(status -> status != null && status.isRunningState())
|
||||
.count();
|
||||
|
||||
long executionsSucceeded = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.map(this::statusOf)
|
||||
.filter(ExecutionStatus.SUCCESS::equals)
|
||||
.count();
|
||||
|
||||
long executionsFailed = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.map(this::statusOf)
|
||||
.filter(ExecutionStatus.ERROR::equals)
|
||||
.count();
|
||||
|
||||
long simulationsStarted = executions.stream()
|
||||
.map(ExecutionEntity::getSnapshot)
|
||||
.filter(snapshot -> snapshot != null && snapshot.isInteractionSimulationEnabled())
|
||||
.count();
|
||||
|
||||
LocalDateTime lastFlowUpdateAt = flows.stream()
|
||||
.map(FlowEntity::getLastUpdateAt)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.max(Comparator.naturalOrder())
|
||||
.orElse(null);
|
||||
|
||||
Long lastExecutionAt = executions.stream()
|
||||
.map(ExecutionEntity::getLastUpdateTime)
|
||||
.max(Long::compareTo)
|
||||
.orElse(null);
|
||||
|
||||
return UserUsageStatsView.builder()
|
||||
.username(username)
|
||||
.flowsCreated(flows.size())
|
||||
.flowsPublished(flows.stream().filter(FlowEntity::isPublished).count())
|
||||
.flowsFinalized(flows.stream().filter(FlowEntity::isFinalized).count())
|
||||
.executionsCreated(executions.size())
|
||||
.executionsRunning(executionsRunning)
|
||||
.executionsSucceeded(executionsSucceeded)
|
||||
.executionsFailed(executionsFailed)
|
||||
.simulationsStarted(simulationsStarted)
|
||||
.lastFlowUpdateAt(lastFlowUpdateAt)
|
||||
.lastExecutionAt(lastExecutionAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ExecutionStatus statusOf(ExecutionSnapshot snapshot) {
|
||||
return snapshot == null ? null : snapshot.getStatus();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package it.cnr.isti.workflow.manager.stats.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class SystemUsageStatsView {
|
||||
private long usersCount;
|
||||
private long flowsCreated;
|
||||
private long flowsPublished;
|
||||
private long flowsFinalized;
|
||||
private long executionsCreated;
|
||||
private long executionsRunning;
|
||||
private long executionsSucceeded;
|
||||
private long executionsFailed;
|
||||
private long simulationsStarted;
|
||||
private LocalDateTime lastFlowUpdateAt;
|
||||
private Long lastExecutionAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package it.cnr.isti.workflow.manager.stats.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class UserUsageStatsView {
|
||||
private String username;
|
||||
private long flowsCreated;
|
||||
private long flowsPublished;
|
||||
private long flowsFinalized;
|
||||
private long executionsCreated;
|
||||
private long executionsRunning;
|
||||
private long executionsSucceeded;
|
||||
private long executionsFailed;
|
||||
private long simulationsStarted;
|
||||
private LocalDateTime lastFlowUpdateAt;
|
||||
private Long lastExecutionAt;
|
||||
}
|
||||
|
|
@ -50,6 +50,10 @@ public class ExecutionControllerTest {
|
|||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExecutionControllerTest.class);
|
||||
|
||||
private LoginEntity testUser() {
|
||||
return new LoginEntity("testuser", "testpassword");
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
|
|
@ -135,7 +139,8 @@ public class ExecutionControllerTest {
|
|||
flow.getDescription(),
|
||||
FlowData.builder().blocks(flow.getBlocks()).connections(flow.getConnections()).build());
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
assert executionObject != null;
|
||||
assert executionObject.getId() != null;
|
||||
|
|
@ -144,18 +149,19 @@ public class ExecutionControllerTest {
|
|||
|
||||
logger.info("execution status is {}",executionObject.getContext().getStatus());
|
||||
|
||||
executionObject = executionsController.prepareStringInputs(executionObject.getId(), first.getId(), first.getInputs().get(0).getName(), "Leonardo da Vinci");
|
||||
executionObject = executionsController.prepareStringInputs(executionObject.getId(), first.getId(),
|
||||
first.getInputs().get(0).getName(), "Leonardo da Vinci", testUser());
|
||||
|
||||
assert executionObject.getContext().getStatus() == ExecutionStatus.READY;
|
||||
|
||||
logger.info("execution status is {}",executionObject.getContext().getStatus());
|
||||
|
||||
executionObject = executionsController.start(executionObject.getId());
|
||||
executionObject = executionsController.start(executionObject.getId(), testUser());
|
||||
|
||||
assert executionObject.getContext().getStatus() == ExecutionStatus.RUNNING;
|
||||
|
||||
while(executionObject.getContext().getStatus().isRunningState()){
|
||||
executionObject = executionsController.get(executionObject.getId());
|
||||
executionObject = executionsController.get(executionObject.getId(), testUser());
|
||||
logger.info("execution status is {}",executionObject.getContext().getStatus());
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
|
|
@ -199,12 +205,13 @@ public class ExecutionControllerTest {
|
|||
flow.getDescription(),
|
||||
FlowData.builder().blocks(flow.getBlocks()).build());
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
executionsController.prepareStringInputs(executionObject.getId(), block.getId(), "name", "Ada");
|
||||
executionObject = executionsController.start(executionObject.getId());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
executionsController.prepareStringInputs(executionObject.getId(), block.getId(), "name", "Ada", testUser());
|
||||
executionObject = executionsController.start(executionObject.getId(), testUser());
|
||||
waitForExecutionStatus(executionObject, ExecutionStatus.SUCCESS);
|
||||
|
||||
List<ExecutionEvent> events = executionsController.getEvents(executionObject.getId());
|
||||
List<ExecutionEvent> events = executionsController.getEvents(executionObject.getId(), testUser());
|
||||
org.junit.jupiter.api.Assertions.assertFalse(events.isEmpty());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(events.stream().anyMatch(event -> event.getType() == ExecutionEventType.LLM_REQUEST));
|
||||
}
|
||||
|
|
@ -213,10 +220,62 @@ public class ExecutionControllerTest {
|
|||
public void getMissingExecutionReturnsNotFound() {
|
||||
ResponseStatusException exception = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> executionsController.get("missing-execution-id"));
|
||||
() -> executionsController.get("missing-execution-id", testUser()));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllExecutionsForUserReturnsOnlyOwnedExecutions() {
|
||||
Block<LLMBlockType> block = blocksController.create(LLMBlockConfiguration.builder()
|
||||
.prompt("Hello, ${{name}}!")
|
||||
.name("owned")
|
||||
.llmDescriptor(LLMDescriptor.builder().provider("testProvider").model("testModel").build())
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
"Owned Executions Flow",
|
||||
"Execution ownership",
|
||||
FlowData.builder().blocks(List.of(block)).build());
|
||||
|
||||
FlowView firstFlow = flowController.createFlow(request, new LoginEntity("owner-a", "pwd")).getBody();
|
||||
FlowView secondFlow = flowController.createFlow(request, new LoginEntity("owner-b", "pwd")).getBody();
|
||||
|
||||
ExecutionView firstExecution = executionsController.create(firstFlow.id(), new LoginEntity("owner-a", "pwd"));
|
||||
ExecutionView secondExecution = executionsController.create(secondFlow.id(), new LoginEntity("owner-b", "pwd"));
|
||||
|
||||
List<ExecutionView> ownerExecutions = executionsController.getAll(new LoginEntity("owner-a", "pwd"));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, ownerExecutions.size());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(firstExecution.getId(), ownerExecutions.getFirst().getId());
|
||||
org.junit.jupiter.api.Assertions.assertNotEquals(secondExecution.getId(), ownerExecutions.getFirst().getId());
|
||||
|
||||
List<ExecutionView> otherUserExecutions = executionsController.getAll(new LoginEntity("owner-b", "pwd"));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, otherUserExecutions.size());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(secondExecution.getId(), otherUserExecutions.getFirst().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionOwnedByAnotherUserReturnsForbidden() {
|
||||
Block<LLMBlockType> block = blocksController.create(LLMBlockConfiguration.builder()
|
||||
.prompt("Hello, ${{name}}!")
|
||||
.name("forbidden")
|
||||
.llmDescriptor(LLMDescriptor.builder().provider("testProvider").model("testModel").build())
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
"Forbidden Execution Flow",
|
||||
"Execution ownership restriction",
|
||||
FlowData.builder().blocks(List.of(block)).build());
|
||||
|
||||
FlowView flow = flowController.createFlow(request, new LoginEntity("owner-a", "pwd")).getBody();
|
||||
ExecutionView execution = executionsController.create(flow.id(), new LoginEntity("owner-a", "pwd"));
|
||||
|
||||
ResponseStatusException exception = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> executionsController.get(execution.getId(), new LoginEntity("owner-b", "pwd")));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertEquals(HttpStatus.FORBIDDEN, exception.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionPayloadDoesNotSerializeEvents() throws JsonProcessingException {
|
||||
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
|
||||
|
|
@ -241,7 +300,8 @@ public class ExecutionControllerTest {
|
|||
flow.getDescription(),
|
||||
FlowData.builder().blocks(flow.getBlocks()).build());
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
String serialized = ObjectMapperHolder.mapper.writeValueAsString(executionObject);
|
||||
JsonNode root = ObjectMapperHolder.mapper.readTree(serialized);
|
||||
|
|
@ -273,7 +333,8 @@ public class ExecutionControllerTest {
|
|||
|
||||
ResponseStatusException exception = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> executionsController.create(createdFlow.getBody().id()));
|
||||
() -> executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword")));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(exception.getReason().contains("\"field\":\"specificConfiguration.llmDescriptor\""));
|
||||
|
|
@ -297,15 +358,17 @@ public class ExecutionControllerTest {
|
|||
FlowData.builder().block(reviewBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
executionsController.prepareStringInputs(executionObject.getId(), reviewBlock.getId(),
|
||||
reviewBlock.getInputs().getFirst().getName(), "Candidate summary");
|
||||
executionsController.start(executionObject.getId());
|
||||
reviewBlock.getInputs().getFirst().getName(), "Candidate summary", testUser());
|
||||
executionsController.start(executionObject.getId(), testUser());
|
||||
|
||||
waitForExecutionStatus(executionObject, ExecutionStatus.WAITING);
|
||||
|
||||
executionsController.provideInteractionValue(executionObject.getId(), reviewBlock.getId(), "output", "Approved");
|
||||
executionsController.provideInteractionValue(executionObject.getId(), reviewBlock.getId(), "output", "Approved",
|
||||
testUser());
|
||||
|
||||
waitForExecutionStatus(executionObject, ExecutionStatus.SUCCESS);
|
||||
}
|
||||
|
|
@ -329,7 +392,8 @@ public class ExecutionControllerTest {
|
|||
FlowData.builder().block(llmBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, executionObject.getRequiredAuthorizations().size());
|
||||
org.junit.jupiter.api.Assertions.assertEquals("LLMProvider::Gemini::authorization",
|
||||
|
|
@ -338,15 +402,15 @@ public class ExecutionControllerTest {
|
|||
String executionId = executionObject.getId();
|
||||
|
||||
executionsController.prepareStringInputs(executionId, llmBlock.getId(),
|
||||
llmBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
llmBlock.getInputs().getFirst().getName(), "Ada Lovelace", testUser());
|
||||
|
||||
IllegalStateException missingAuthorization = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executionsController.start(executionId));
|
||||
() -> executionsController.start(executionId, testUser()));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(missingAuthorization.getMessage().contains("LLMProvider::Gemini::authorization"));
|
||||
|
||||
executionObject = executionsController.provideAuthorization(executionId,
|
||||
new ExecutionAuthorizationValueRequest("LLMProvider::Gemini::authorization", "test-api-key"));
|
||||
new ExecutionAuthorizationValueRequest("LLMProvider::Gemini::authorization", "test-api-key"), testUser());
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertTrue(executionObject.getMissingAuthorizationKeys().isEmpty());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.READY, executionObject.getContext().getStatus());
|
||||
|
|
@ -370,14 +434,15 @@ public class ExecutionControllerTest {
|
|||
FlowData.builder().block(reviewBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
executionsController.prepareStringInputs(executionObject.getId(), reviewBlock.getId(),
|
||||
reviewBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
executionsController.start(executionObject.getId());
|
||||
reviewBlock.getInputs().getFirst().getName(), "Ada Lovelace", testUser());
|
||||
executionsController.start(executionObject.getId(), testUser());
|
||||
waitForExecutionStatus(executionObject, ExecutionStatus.WAITING);
|
||||
|
||||
ExecutionView cancelled = executionsController.cancel(executionObject.getId());
|
||||
ExecutionView cancelled = executionsController.cancel(executionObject.getId(), testUser());
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.CANCELLED, cancelled.getContext().getStatus());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(cancelled.getContext().getStatus().isFinalState());
|
||||
|
|
@ -406,23 +471,25 @@ public class ExecutionControllerTest {
|
|||
FlowData.builder().block(reviewBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
executionsController.prepareStringInputs(executionObject.getId(), reviewBlock.getId(),
|
||||
reviewBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
executionsController.start(executionObject.getId());
|
||||
reviewBlock.getInputs().getFirst().getName(), "Ada Lovelace", testUser());
|
||||
executionsController.start(executionObject.getId(), testUser());
|
||||
waitForExecutionStatus(executionObject, ExecutionStatus.WAITING);
|
||||
|
||||
String executionId = executionObject.getId();
|
||||
executionsService.clearInMemoryExecutions();
|
||||
|
||||
ExecutionView suspended = executionsController.get(executionId);
|
||||
ExecutionView suspended = executionsController.get(executionId, testUser());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.SUSPENDED, suspended.getContext().getStatus());
|
||||
|
||||
ExecutionView resumed = executionsController.resume(executionId);
|
||||
ExecutionView resumed = executionsController.resume(executionId, testUser());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.WAITING, resumed.getContext().getStatus());
|
||||
|
||||
resumed = executionsController.provideInteractionValue(executionId, reviewBlock.getId(), "output", "Approved");
|
||||
resumed = executionsController.provideInteractionValue(executionId, reviewBlock.getId(), "output", "Approved",
|
||||
testUser());
|
||||
waitForExecutionStatus(resumed, ExecutionStatus.SUCCESS);
|
||||
}
|
||||
|
||||
|
|
@ -450,14 +517,15 @@ public class ExecutionControllerTest {
|
|||
FlowData.builder().block(chatBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(executionObject.isSimulationAvailable());
|
||||
|
||||
executionsController.prepareStringInputs(executionObject.getId(), chatBlock.getId(),
|
||||
chatBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
chatBlock.getInputs().getFirst().getName(), "Ada Lovelace", testUser());
|
||||
|
||||
executionObject = executionsController.simulate(executionObject.getId(),
|
||||
new ExecutionSimulationRequest(simulatorDescriptor));
|
||||
new ExecutionSimulationRequest(simulatorDescriptor), testUser());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(executionObject.isInteractionSimulationEnabled());
|
||||
waitForExecutionStatus(executionObject, ExecutionStatus.SUCCESS);
|
||||
}
|
||||
|
|
@ -481,12 +549,13 @@ public class ExecutionControllerTest {
|
|||
FlowData.builder().block(llmBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertFalse(executionObject.isSimulationAvailable());
|
||||
|
||||
executionsController.prepareStringInputs(executionObject.getId(), llmBlock.getId(),
|
||||
llmBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
llmBlock.getInputs().getFirst().getName(), "Ada Lovelace", testUser());
|
||||
|
||||
ResponseStatusException exception = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
ResponseStatusException.class,
|
||||
|
|
@ -494,15 +563,15 @@ public class ExecutionControllerTest {
|
|||
new ExecutionSimulationRequest(LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("simulateModel")
|
||||
.build())));
|
||||
.build()), testUser()));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.READY, executionObject.getContext().getStatus());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.CREATED, executionObject.getContext().getStatus());
|
||||
}
|
||||
|
||||
private void waitForExecutionStatus(ExecutionView executionObject, ExecutionStatus expectedStatus) {
|
||||
long deadline = System.currentTimeMillis() + 5_000;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
executionObject = executionsController.get(executionObject.getId());
|
||||
executionObject = executionsController.get(executionObject.getId(), testUser());
|
||||
if (executionObject.getContext().getStatus() == expectedStatus) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,366 @@
|
|||
package it.cnr.isti.workflow.manager.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import it.cnr.isti.workflow.manager.auth.model.UserRole;
|
||||
import it.cnr.isti.workflow.manager.auth.repo.AuthRepository;
|
||||
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionStatus;
|
||||
import it.cnr.isti.workflow.manager.executions.persistence.ExecutionSnapshot;
|
||||
import it.cnr.isti.workflow.manager.executions.repo.ExecutionEntity;
|
||||
import it.cnr.isti.workflow.manager.executions.repo.ExecutionRepository;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowEntity;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowRepository;
|
||||
import it.cnr.isti.workflow.manager.stats.model.SystemUsageStatsView;
|
||||
import it.cnr.isti.workflow.manager.stats.model.UserUsageStatsView;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
public class StatsControllerTest {
|
||||
|
||||
@Autowired
|
||||
private StatsController statsController;
|
||||
|
||||
@Autowired
|
||||
private AuthRepository authRepository;
|
||||
|
||||
@Autowired
|
||||
private FlowRepository flowRepository;
|
||||
|
||||
@Autowired
|
||||
private ExecutionRepository executionRepository;
|
||||
|
||||
@Test
|
||||
public void nonAdminCannotReadOwnUsageStats() {
|
||||
authRepository.save(new LoginEntity("statsuser", "hashed", "statsuser@example.com", UserRole.USER));
|
||||
|
||||
flowRepository.save(FlowEntity.builder()
|
||||
.name("Flow One")
|
||||
.owner("statsuser")
|
||||
.description("first")
|
||||
.createdAt(LocalDateTime.now().minusDays(2))
|
||||
.lastUpdateAt(LocalDateTime.now().minusDays(1))
|
||||
.published(true)
|
||||
.finalized(true)
|
||||
.flow(FlowData.builder().build())
|
||||
.build());
|
||||
|
||||
flowRepository.save(FlowEntity.builder()
|
||||
.name("Flow Two")
|
||||
.owner("statsuser")
|
||||
.description("second")
|
||||
.createdAt(LocalDateTime.now().minusHours(10))
|
||||
.lastUpdateAt(LocalDateTime.now())
|
||||
.published(false)
|
||||
.finalized(false)
|
||||
.flow(FlowData.builder().build())
|
||||
.build());
|
||||
|
||||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id("exec-1")
|
||||
.name("Execution One")
|
||||
.owner("statsuser")
|
||||
.creationTime(100L)
|
||||
.lastUpdateTime(200L)
|
||||
.flow(FlowData.builder().build())
|
||||
.snapshot(ExecutionSnapshot.builder()
|
||||
.status(ExecutionStatus.SUCCESS)
|
||||
.interactionSimulationEnabled(true)
|
||||
.providedAuthorizations(Map.of())
|
||||
.build())
|
||||
.build());
|
||||
|
||||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id("exec-2")
|
||||
.name("Execution Two")
|
||||
.owner("statsuser")
|
||||
.creationTime(300L)
|
||||
.lastUpdateTime(400L)
|
||||
.flow(FlowData.builder().build())
|
||||
.snapshot(ExecutionSnapshot.builder()
|
||||
.status(ExecutionStatus.ERROR)
|
||||
.interactionSimulationEnabled(false)
|
||||
.providedAuthorizations(Map.of())
|
||||
.build())
|
||||
.build());
|
||||
|
||||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id("exec-3")
|
||||
.name("Execution Three")
|
||||
.owner("statsuser")
|
||||
.creationTime(500L)
|
||||
.lastUpdateTime(600L)
|
||||
.flow(FlowData.builder().build())
|
||||
.snapshot(ExecutionSnapshot.builder()
|
||||
.status(ExecutionStatus.RUNNING)
|
||||
.interactionSimulationEnabled(false)
|
||||
.providedAuthorizations(Map.of())
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ResponseEntity<UserUsageStatsView> response = statsController.getUserStats(
|
||||
"statsuser",
|
||||
new LoginEntity("statsuser", "ignored", "statsuser@example.com", UserRole.USER));
|
||||
|
||||
assertEquals(403, response.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminCanReadOtherUserUsageStats() {
|
||||
authRepository.save(new LoginEntity("targetuser", "hashed", "target@example.com", UserRole.USER));
|
||||
authRepository.save(new LoginEntity("adminstats", "hashed", "admin@example.com", UserRole.ADMIN));
|
||||
|
||||
ResponseEntity<UserUsageStatsView> response = statsController.getUserStats(
|
||||
"targetuser",
|
||||
new LoginEntity("adminstats", "ignored", "admin@example.com", UserRole.ADMIN));
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals("targetuser", response.getBody().getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminCanReadOwnUsageStats() {
|
||||
authRepository.save(new LoginEntity("adminstats3", "hashed", "admin3@example.com", UserRole.ADMIN));
|
||||
|
||||
flowRepository.save(FlowEntity.builder()
|
||||
.name("Flow One")
|
||||
.owner("adminstats3")
|
||||
.description("first")
|
||||
.createdAt(LocalDateTime.now().minusDays(2))
|
||||
.lastUpdateAt(LocalDateTime.now().minusDays(1))
|
||||
.published(true)
|
||||
.finalized(true)
|
||||
.flow(FlowData.builder().build())
|
||||
.build());
|
||||
|
||||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id("exec-admin-1")
|
||||
.name("Execution One")
|
||||
.owner("adminstats3")
|
||||
.creationTime(100L)
|
||||
.lastUpdateTime(200L)
|
||||
.flow(FlowData.builder().build())
|
||||
.snapshot(ExecutionSnapshot.builder()
|
||||
.status(ExecutionStatus.SUCCESS)
|
||||
.interactionSimulationEnabled(true)
|
||||
.providedAuthorizations(Map.of())
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ResponseEntity<UserUsageStatsView> response = statsController.getUserStats(
|
||||
"adminstats3",
|
||||
new LoginEntity("adminstats3", "ignored", "admin3@example.com", UserRole.ADMIN));
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals("adminstats3", response.getBody().getUsername());
|
||||
assertEquals(1L, response.getBody().getFlowsCreated());
|
||||
assertEquals(1L, response.getBody().getFlowsPublished());
|
||||
assertEquals(1L, response.getBody().getFlowsFinalized());
|
||||
assertEquals(1L, response.getBody().getExecutionsCreated());
|
||||
assertEquals(0L, response.getBody().getExecutionsRunning());
|
||||
assertEquals(1L, response.getBody().getExecutionsSucceeded());
|
||||
assertEquals(0L, response.getBody().getExecutionsFailed());
|
||||
assertEquals(1L, response.getBody().getSimulationsStarted());
|
||||
assertEquals(200L, response.getBody().getLastExecutionAt());
|
||||
assertNotNull(response.getBody().getLastFlowUpdateAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deletedUserStillAppearsInUserStats() {
|
||||
LoginEntity deletedUser = new LoginEntity("deletedstats", "hashed", "deletedstats@example.com", UserRole.USER);
|
||||
deletedUser.setActive(false);
|
||||
authRepository.save(deletedUser);
|
||||
authRepository.save(new LoginEntity("adminstatsDeleted", "hashed", "admin-deleted@example.com", UserRole.ADMIN));
|
||||
|
||||
flowRepository.save(FlowEntity.builder()
|
||||
.name("Deleted Flow")
|
||||
.owner("deletedstats")
|
||||
.description("deleted")
|
||||
.createdAt(LocalDateTime.now().minusDays(1))
|
||||
.lastUpdateAt(LocalDateTime.now().minusHours(1))
|
||||
.published(false)
|
||||
.finalized(false)
|
||||
.flow(FlowData.builder().build())
|
||||
.build());
|
||||
|
||||
ResponseEntity<UserUsageStatsView> response = statsController.getUserStats(
|
||||
"deletedstats",
|
||||
new LoginEntity("adminstatsDeleted", "ignored", "admin-deleted@example.com", UserRole.ADMIN));
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals("deletedstats", response.getBody().getUsername());
|
||||
assertEquals(1L, response.getBody().getFlowsCreated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminCanReadAllUserUsageStats() {
|
||||
authRepository.save(new LoginEntity("usera", "hashed", "usera@example.com", UserRole.USER));
|
||||
authRepository.save(new LoginEntity("userb", "hashed", "userb@example.com", UserRole.USER));
|
||||
authRepository.save(new LoginEntity("adminstats4", "hashed", "admin4@example.com", UserRole.ADMIN));
|
||||
|
||||
flowRepository.save(FlowEntity.builder()
|
||||
.name("Flow A")
|
||||
.owner("usera")
|
||||
.description("a")
|
||||
.createdAt(LocalDateTime.now().minusDays(1))
|
||||
.lastUpdateAt(LocalDateTime.now().minusHours(2))
|
||||
.published(true)
|
||||
.finalized(false)
|
||||
.flow(FlowData.builder().build())
|
||||
.build());
|
||||
|
||||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id("exec-list-1")
|
||||
.name("Execution A")
|
||||
.owner("usera")
|
||||
.creationTime(10L)
|
||||
.lastUpdateTime(20L)
|
||||
.flow(FlowData.builder().build())
|
||||
.snapshot(ExecutionSnapshot.builder()
|
||||
.status(ExecutionStatus.SUCCESS)
|
||||
.interactionSimulationEnabled(false)
|
||||
.providedAuthorizations(Map.of())
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ResponseEntity<List<String>> response = statsController.getAllUserStats(
|
||||
new LoginEntity("adminstats4", "ignored", "admin4@example.com", UserRole.ADMIN));
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertNotNull(response.getBody());
|
||||
assertFalse(response.getBody().isEmpty());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(response.getBody().contains("adminstats4"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(response.getBody().contains("usera"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminCanReadSystemUsageStats() {
|
||||
authRepository.save(new LoginEntity("systemadmin", "hashed", "systemadmin@example.com", UserRole.ADMIN));
|
||||
authRepository.save(new LoginEntity("systemuser", "hashed", "systemuser@example.com", UserRole.USER));
|
||||
|
||||
flowRepository.save(FlowEntity.builder()
|
||||
.name("Flow S1")
|
||||
.owner("systemuser")
|
||||
.description("s1")
|
||||
.createdAt(LocalDateTime.now().minusDays(1))
|
||||
.lastUpdateAt(LocalDateTime.now().minusHours(4))
|
||||
.published(true)
|
||||
.finalized(false)
|
||||
.flow(FlowData.builder().build())
|
||||
.build());
|
||||
|
||||
flowRepository.save(FlowEntity.builder()
|
||||
.name("Flow S2")
|
||||
.owner("systemadmin")
|
||||
.description("s2")
|
||||
.createdAt(LocalDateTime.now().minusHours(12))
|
||||
.lastUpdateAt(LocalDateTime.now().minusHours(1))
|
||||
.published(false)
|
||||
.finalized(true)
|
||||
.flow(FlowData.builder().build())
|
||||
.build());
|
||||
|
||||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id("exec-system-1")
|
||||
.name("Execution S1")
|
||||
.owner("systemuser")
|
||||
.creationTime(10L)
|
||||
.lastUpdateTime(20L)
|
||||
.flow(FlowData.builder().build())
|
||||
.snapshot(ExecutionSnapshot.builder()
|
||||
.status(ExecutionStatus.SUCCESS)
|
||||
.interactionSimulationEnabled(true)
|
||||
.providedAuthorizations(Map.of())
|
||||
.build())
|
||||
.build());
|
||||
|
||||
executionRepository.save(ExecutionEntity.builder()
|
||||
.id("exec-system-2")
|
||||
.name("Execution S2")
|
||||
.owner("systemadmin")
|
||||
.creationTime(30L)
|
||||
.lastUpdateTime(40L)
|
||||
.flow(FlowData.builder().build())
|
||||
.snapshot(ExecutionSnapshot.builder()
|
||||
.status(ExecutionStatus.ERROR)
|
||||
.interactionSimulationEnabled(false)
|
||||
.providedAuthorizations(Map.of())
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ResponseEntity<SystemUsageStatsView> response = statsController.getSystemStats(
|
||||
new LoginEntity("systemadmin", "ignored", "systemadmin@example.com", UserRole.ADMIN));
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals(2L, response.getBody().getFlowsCreated());
|
||||
assertEquals(1L, response.getBody().getFlowsPublished());
|
||||
assertEquals(1L, response.getBody().getFlowsFinalized());
|
||||
assertEquals(2L, response.getBody().getExecutionsCreated());
|
||||
assertEquals(0L, response.getBody().getExecutionsRunning());
|
||||
assertEquals(1L, response.getBody().getExecutionsSucceeded());
|
||||
assertEquals(1L, response.getBody().getExecutionsFailed());
|
||||
assertEquals(1L, response.getBody().getSimulationsStarted());
|
||||
assertEquals(40L, response.getBody().getLastExecutionAt());
|
||||
assertNotNull(response.getBody().getLastFlowUpdateAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAdminCannotReadSystemUsageStats() {
|
||||
authRepository.save(new LoginEntity("plainsystemuser", "hashed", "plainsystem@example.com", UserRole.USER));
|
||||
|
||||
ResponseEntity<SystemUsageStatsView> response = statsController.getSystemStats(
|
||||
new LoginEntity("plainsystemuser", "ignored", "plainsystem@example.com", UserRole.USER));
|
||||
|
||||
assertEquals(403, response.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAdminCannotReadAllUserUsageStats() {
|
||||
authRepository.save(new LoginEntity("plainuser", "hashed", "plain@example.com", UserRole.USER));
|
||||
|
||||
ResponseEntity<List<String>> response = statsController.getAllUserStats(
|
||||
new LoginEntity("plainuser", "ignored", "plain@example.com", UserRole.USER));
|
||||
|
||||
assertEquals(403, response.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAdminCannotReadOtherUserUsageStats() {
|
||||
authRepository.save(new LoginEntity("targetuser2", "hashed", "target2@example.com", UserRole.USER));
|
||||
authRepository.save(new LoginEntity("otheruser2", "hashed", "other2@example.com", UserRole.USER));
|
||||
|
||||
ResponseEntity<UserUsageStatsView> response = statsController.getUserStats(
|
||||
"targetuser2",
|
||||
new LoginEntity("otheruser2", "ignored", "other2@example.com", UserRole.USER));
|
||||
|
||||
assertEquals(403, response.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingUserStatsReturnNotFound() {
|
||||
authRepository.save(new LoginEntity("adminstats2", "hashed", "admin2@example.com", UserRole.ADMIN));
|
||||
|
||||
ResponseEntity<UserUsageStatsView> response = statsController.getUserStats(
|
||||
"missing-user",
|
||||
new LoginEntity("adminstats2", "ignored", "admin2@example.com", UserRole.ADMIN));
|
||||
|
||||
assertEquals(404, response.getStatusCode().value());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue