solved an issue on flow save

This commit is contained in:
Lucio Lelii 2025-05-16 22:10:13 +02:00
parent 802be88452
commit e23eeb3d9c
12 changed files with 333 additions and 70 deletions

View File

@ -79,10 +79,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>

View File

@ -0,0 +1,30 @@
package it.cnr.isti.workflow.manager.configurations;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
final String securitySchemeName = "bearerAuth";
return new OpenAPI()
.info(new Info().title("API Docs").version("v1"))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}
}

View File

@ -14,7 +14,6 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@ -26,17 +25,19 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers(HttpMethod.GET,"/health").permitAll()
.requestMatchers(HttpMethod.GET,"/types/nodes/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
//.requestMatchers(HttpMethod.GET, "/health").permitAll()
.requestMatchers(HttpMethod.GET, "/types/nodes/**").permitAll()
.requestMatchers("/swagger-ui.html").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean

View File

@ -8,14 +8,17 @@ import java.util.stream.Collectors;
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.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import it.cnr.isti.workflow.manager.exceptions.WebException;
import it.cnr.isti.workflow.manager.executors.ExecutionObject;
import it.cnr.isti.workflow.manager.model.ExecutionContext;
import it.cnr.isti.workflow.manager.model.flows.Flow;
@ -28,6 +31,44 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* The ExecutionController class provides RESTful endpoints for managing
* execution objects and their associated contexts and inputs. It allows
* clients to create, retrieve, update, and delete execution objects, as
* well as prepare inputs for executions.
*
* <p>Endpoints include:
* <ul>
* <li>Creating a new execution for a given flow</li>
* <li>Retrieving all executions or a specific execution by ID</li>
* <li>Retrieving all execution contexts</li>
* <li>Starting an execution</li>
* <li>Preparing string or file inputs for an execution</li>
* <li>Deleting an execution</li>
* </ul>
*
* <p>Exception handling is provided for cases where a requested flow or
* execution is not found, returning appropriate HTTP status codes.
*
* <p>Security is enforced using the "bearerAuth" security requirement.
*
* <p>Dependencies:
* <ul>
* <li>{@link FlowRepository} for accessing flow data</li>
* <li>{@link ExecutionService} for managing execution logic</li>
* </ul>
*
* <p>Logging is performed using SLF4J to provide insights into the operations
* performed by the controller.
*
* <p>Annotations:
* <ul>
* <li>{@code @RestController} to define this class as a REST controller</li>
* <li>{@code @RequestMapping("/executions")} to map requests to the "/executions" path</li>
* <li>{@code @SecurityRequirement(name = "bearerAuth")} to enforce security</li>
* </ul>
*/
@SecurityRequirement(name = "bearerAuth")
@RestController
@RequestMapping("/executions")
public class ExecutionController {
@ -40,6 +81,16 @@ public class ExecutionController {
@Autowired
ExecutionService executionService;
/**
* Creates a new execution object for the specified flow.
*
* @param flowId the ID of the flow for which the execution is to be created
* @return the created {@link ExecutionObject}
* @throws IllegalArgumentException if the flow with the specified ID is not found
* @throws WebServerException if an error occurs while creating the execution
*/
@Operation(summary = "Creates an execution", description = "Creates a new execution object for the specified flow.")
@PostMapping()
public ExecutionObject create(@RequestBody String flowId) {
logger.info("Creating execution for flow {}", flowId);
@ -50,39 +101,104 @@ public class ExecutionController {
return eo;
} catch (Throwable e) {
logger.error("Error creating execution for flow {}", flowId, e);
throw new WebServerException("Error while creating execution", e);
throw new WebException(HttpStatus.INTERNAL_SERVER_ERROR,"Error while creating execution");
}
}
/**
* Retrieves a list of all executions.
*
* @return a list of {@link ExecutionObject} representing all executions.
*/
@Operation(summary = "Retrieves executions", description = "Retrieves a list of all executions")
@GetMapping()
public List<ExecutionObject> getAll() {
return executionService.getAllExecutions();
}
/**
* Retrieves all execution contexts.
*
* @return a map of execution IDs to their corresponding {@link ExecutionContext}
*/
@Operation(summary = "Retrieves contexts", description = "Retrieves all execution contexts")
@GetMapping(path = "contexts")
public Map<String,ExecutionContext> getAllContexts() {
return executionService.getAllExecutions().stream().collect(Collectors.toMap(ExecutionObject::getId, ExecutionObject::getContext));
}
@GetMapping(path = "{id}")
/**
* Retrieves a specific execution by its ID.
*
* @param id the ID of the execution to retrieve
* @return the {@link ExecutionObject} with the specified ID
* @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 ExecutionObject get(@PathVariable String id) {
return executionService.getExecution(id);
}
/**
* Deletes an execution with the specified ID.
*
* @param id the unique identifier of the execution to be deleted
* @return a ResponseEntity indicating the result of the operation
*/
@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);
return ResponseEntity.ok().build();
}
/**
* Starts the execution of a workflow with the given ID.
*
* @param id the unique identifier of the workflow to start
* @return the {@link ExecutionObject} representing the started execution
*/
@PutMapping(path = "{id}/start")
@Operation(summary = "Starts an execution", description = "Starts the execution of a workflow with the given ID")
public ExecutionObject start(@PathVariable String id) {
return executionService.startExecution(id);
}
/**
* Prepares the input for an execution by associating a given input string
* with a specific input name and execution ID.
*
* @param input The input string to be prepared and associated.
* @param inputName The name of the input to be associated with the execution.
* @param id The unique identifier of the execution.
* @return An {@link ExecutionObject} representing the updated execution state
* after preparing the input.
*/
@PutMapping(path = "{id}/input/{inputName}")
@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 ExecutionObject prepareStringInputs(@RequestBody String input, @PathVariable String inputName,
@PathVariable String id) {
return executionService.prepareInput(id, inputName, input);
}
/**
* Prepares file inputs for a specific execution by uploading a file and associating it
* with the given input name and execution ID.
*
* @param file The multipart file to be uploaded and processed.
* @param inputName The name of the input to associate the file with.
* @param id The ID of the execution to which the input belongs.
* @return An {@link ExecutionObject} representing the updated execution state after
* preparing the input.
* @throws WebServerException If an error occurs while creating or transferring the file.
*/
@PutMapping(path = "{id}/input/{inputName}", consumes = "multipart/form-data")
@ResponseBody
@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 ExecutionObject prepareFileInputs(@RequestParam("file") MultipartFile file, @PathVariable String inputName,
@PathVariable String id) {
try {
@ -95,8 +211,4 @@ public class ExecutionController {
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<?> executionNotFound(IllegalArgumentException exc) {
return ResponseEntity.notFound().build();
}
}

View File

@ -10,12 +10,12 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import it.cnr.isti.workflow.manager.exceptions.WebException;
import it.cnr.isti.workflow.manager.model.auth.LoginEntity;
import it.cnr.isti.workflow.manager.model.flows.Flow;
@ -29,7 +29,32 @@ import org.springframework.web.bind.annotation.GetMapping;
/**
* Controller for managing flows.
*
* This controller provides endpoints for creating, updating, retrieving, and listing flows.
* It enforces security requirements and ensures that users can only access or modify flows
* they have created or that are marked as public.
*
* Endpoints:
* - POST /flows: Create a new flow.
* - PUT /flows/{id}: Update an existing flow.
* - GET /flows: Retrieve all flows created by the user or public flows.
* - GET /flows/{id}: Retrieve a specific flow by its ID.
*
* Security:
* - Requires bearer authentication.
* - Users can only modify or retrieve flows they created unless the flow is public.
*
* Dependencies:
* - FlowRepository: Repository for accessing and managing flow data.
* - Logger: Used for logging flow creation and updates.
*
* Exceptions:
* - WebException: Thrown for various HTTP error scenarios, such as invalid input,
* unauthorized access, or resource not found.
*/
@SecurityRequirement(name = "bearerAuth")
@RestController
@RequestMapping("/flows")
public class FlowsController {
@ -40,7 +65,20 @@ public class FlowsController {
FlowRepository flowRepository;
/**
* Creates a new flow.
*
* This method handles HTTP POST requests with a JSON payload to create a new flow.
* It validates that the flow ID is null (as it must be generated during creation),
* sets the creator's username, and saves the flow to the repository.
*
* @param flow The flow object to be created, provided in the request body.
* @param userDetails The authenticated user's details, used to set the creator of the flow.
* @return The saved flow object with its generated ID and other persisted details.
* @throws WebException If the flow ID is not null, indicating an invalid creation request.
*/
@PostMapping(consumes = "application/json")
@Operation(summary = "Create a new flow", description = "Creates a new flow in the system.")
public Flow createFlow(@RequestBody Flow flow, @AuthenticationPrincipal LoginEntity userDetails) {
//validateFlow(flow);
if (flow.getId() != null)
@ -52,7 +90,19 @@ public class FlowsController {
return savedFlow;
}
/**
* Updates and saves an existing Flow entity.
*
* @param id The ID of the Flow to be updated. Must match the ID of the provided Flow object.
* @param flow The Flow object containing the updated data.
* @param userDetails The authenticated user's details, used to verify ownership of the Flow.
* @return The updated and saved Flow object.
* @throws WebException If the provided Flow ID does not match the path variable ID (BAD_REQUEST),
* if the Flow is not found (NOT_FOUND),
* or if the authenticated user is not the creator of the Flow (FORBIDDEN).
*/
@PutMapping(path = "{id}", consumes = "application/json")
@Operation(summary = "Update an existing flow", description = "Updates an existing flow in the system.")
public Flow saveFlow(@PathVariable String id, @RequestBody Flow flow, @AuthenticationPrincipal LoginEntity userDetails) {
//validateFlow(flow);
if (!id.equals(flow.getId()))
@ -62,12 +112,26 @@ public class FlowsController {
if (!savedFlow.getCreatedBy().equals(userDetails.getUsername()))
throw new WebException(HttpStatus.FORBIDDEN, "Flow not created by user");
savedFlow = flowRepository.save(savedFlow);
savedFlow = flowRepository.save(flow);
logger.info("Flow saved: {} ",savedFlow.getId());
return savedFlow;
}
/**
* Retrieves all flows created by the authenticated user or public flows.
*
* <p>This endpoint is mapped to a GET request and provides a list of flows
* that are either created by the currently authenticated user or are marked
* as public. The method uses the user's authentication details to filter
* the flows accordingly.</p>
*
* @param userDetails The authentication principal containing the details of
* the currently authenticated user.
* @return A list of {@link Flow} objects representing the flows accessible
* to the authenticated user.
*/
@GetMapping()
@Operation(summary = "Get all flows", description = "Retrieves all flows created by the user or public flows.")
public List<Flow> getAllFlows(@AuthenticationPrincipal LoginEntity userDetails) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println("Authentication: " + auth);
@ -77,6 +141,21 @@ public class FlowsController {
}
@GetMapping("{id}")
@Operation(summary = "Get a flow by ID", description = "Retrieves a specific flow by its ID.")
/**
* Retrieves a specific flow by its ID.
*
* <p>This endpoint is mapped to a GET request and allows the retrieval of
* a flow based on its unique identifier. The method checks if the flow
* exists and whether the authenticated user has permission to access it.</p>
*
* @param id The ID of the flow to be retrieved.
* @param userDetails The authentication principal containing the details of
* the currently authenticated user.
* @return The {@link Flow} object representing the requested flow.
* @throws WebException If the flow is not found (NOT_FOUND) or if the user
* does not have permission to access it (FORBIDDEN).
*/
public Flow getFlow(@PathVariable String id, @AuthenticationPrincipal LoginEntity userDetails) {
Flow flowEntity = flowRepository.findById(id).orElseThrow(() -> new WebException(HttpStatus.NOT_FOUND,"Flow not found"));
if (!flowEntity.getCreatedBy().equals(userDetails.getUsername()) && !flowEntity.isPublic())

View File

@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import it.cnr.isti.workflow.manager.exceptions.WebException;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import it.cnr.isti.workflow.manager.executors.Executor;
import it.cnr.isti.workflow.manager.model.ExecutorDescriptor;
import it.cnr.isti.workflow.manager.model.types.NodeDefinition;
@ -29,46 +29,32 @@ public class NodeDefinitionController {
@Autowired
private List<Executor> executors;
@SecurityRequirement(name = "bearerAuth")
@GetMapping("/nodes")
public List<NodeDefinition> getNodes() {
try {
return (List<NodeDefinition>) repository.findAll();
} catch (Throwable e) {
logger.error("Error retrieving nodes", e);
throw new RuntimeException("Error retrieving nodes", e);
}
return (List<NodeDefinition>) repository.findAll();
}
@GetMapping("/nodes/types")
public List<String> getStoredNodeTypes() {
try {
return repository.getUsedTypes();
} catch (Throwable e) {
logger.error("Error retrieving stored types", e);
throw new WebException(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving stored types");
}
return repository.getUsedTypes();
}
@GetMapping("/nodes/categories")
public List<String> getStoredNodeCategories() {
try {
return repository.getCategories();
} catch (Throwable e) {
logger.error("Error retrieving stored categories", e);
throw new WebException(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving stored categories");
}
return repository.getCategories();
}
@SecurityRequirement(name = "bearerAuth")
@PostMapping("/nodes")
void addNode(@RequestBody NodeDefinition node) {
try {
repository.save(node);
} catch (Exception e) {
logger.error("Error saving node", e);
throw new RuntimeException("Error saving node", e);
}
repository.save(node);
}
@SecurityRequirement(name = "bearerAuth")
@GetMapping("/executors")
public List<ExecutorDescriptor> getAvailableExecutors() {
return executors.stream().map(entry -> entry.getDescriptor(null)).toList();

View File

@ -1,16 +1,23 @@
package it.cnr.isti.workflow.manager.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(WebException.class)
public ResponseEntity<String> handleWebException(WebException ex) {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleNotFound(ResourceNotFoundException ex, WebRequest request) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
return ResponseEntity
.status(ex.getStatus())
.body(ex.getMessage());
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An unexpected error occurred: " + ex.getMessage());
}
}

View File

@ -0,0 +1,24 @@
package it.cnr.isti.workflow.manager.exceptions;
public class InvalidStateException extends Exception {
private static final long serialVersionUID = 1L;
public InvalidStateException(String message) {
super(message);
}
public InvalidStateException(String message, Throwable cause) {
super(message, cause);
}
public InvalidStateException(Throwable cause) {
super(cause);
}
public InvalidStateException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -0,0 +1,14 @@
package it.cnr.isti.workflow.manager.exceptions;
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -13,4 +13,4 @@ public class WebException extends RuntimeException {
public HttpStatus getStatus() {
return status;
}
}
}

View File

@ -14,22 +14,29 @@ import lombok.Setter;
public class ExecutionContext {
public enum Status {
CREATED(true),
INITIALIZING(true),
READY(true),
RUNNING(false),
SUCCESS(false),
ERROR(false);
CREATED(true, false),
INITIALIZING(true, false),
READY(true, false),
RUNNING(false, false),
SUCCESS(false, true),
ERROR(false, true);
private boolean initState;
Status(boolean initState) {
private boolean finalState;
Status(boolean initState, boolean finalState) {
this.initState = initState;
this.finalState = finalState;
}
public boolean isInitState() {
return initState;
}
public boolean isFinaltate() {
return finalState;
}
}
Map<String, Object> inputs = new HashMap<>();

View File

@ -6,7 +6,6 @@ import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import it.cnr.isti.workflow.manager.executors.ExecutionObject;
import it.cnr.isti.workflow.manager.model.ExecutionContext.Status;
import it.cnr.isti.workflow.manager.model.flows.Flow;
@ -26,7 +25,10 @@ public class ExecutionService {
}
public ExecutionObject getExecution(String id) {
return executions.get(id);
ExecutionObject toReturn = executions.get(id);
if (toReturn == null)
throw new IllegalArgumentException("Execution with id " + id + " not found");
return toReturn;
}
public List<ExecutionObject> getAllExecutions() {
@ -34,14 +36,16 @@ public class ExecutionService {
}
public void removeExecution(String id) {
if (!executions.containsKey(id))
throw new IllegalArgumentException("Execution with id " + id + " not found");
if (executions.get(id).getContext().getStatus() == Status.RUNNING)
throw new IllegalStateException("Execution with id "+id+" is still running");
executions.remove(id);
}
public ExecutionObject prepareInput(String id, String key, Object input) {
ExecutionObject eo = getExecution(id);
if (eo == null)
throw new IllegalArgumentException("Execution with id " + id + " not found");
if (!eo.getContext().getStatus().isInitState()) {
throw new IllegalStateException("Execution with id " + id
+ " is not in initialization status (CURRENT STATUS is " + eo.getContext().getStatus() + ")");
@ -58,8 +62,7 @@ public class ExecutionService {
public ExecutionObject startExecution(String id) {
ExecutionObject eo = getExecution(id);
if (eo == null)
throw new IllegalArgumentException("Execution with id " + id + " not found");
if (eo.getContext().getStatus() != Status.READY) {
throw new IllegalStateException("Execution with id " + id + " is not in READY status (CURRENT STATUS is "
+ eo.getContext().getStatus() + ")");