Add flow publication and finalization controls

This commit is contained in:
Lucio Lelii 2026-03-26 13:01:14 +01:00
parent ee14e5d7d1
commit 8c26b5d8fc
4 changed files with 248 additions and 0 deletions

View File

@ -23,6 +23,7 @@ import it.cnr.isti.workflow.manager.flows.FlowAccessDeniedException;
import it.cnr.isti.workflow.manager.flows.FlowNotFoundException;
import it.cnr.isti.workflow.manager.flows.FlowService;
import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest;
import it.cnr.isti.workflow.manager.flows.model.FlowFlagUpdateRequest;
import it.cnr.isti.workflow.manager.flows.model.FlowView;
@ -118,4 +119,36 @@ public class FlowController {
}
}
@PutMapping("/{id}/published")
@Operation(summary = "Update published flag", description = "Updates the published flag of a flow owned by the authenticated user.")
public ResponseEntity<FlowView> updatePublished(@PathVariable String id,
@RequestBody @Valid FlowFlagUpdateRequest request,
@AuthenticationPrincipal LoginEntity userDetails) {
try {
return ResponseEntity.ok(flowService.updatePublished(id, userDetails.getUsername(), request));
} catch (FlowNotFoundException e) {
logger.warn("Update published flag for flow {} failed for user {}: not found", id, userDetails.getUsername());
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
} catch (FlowAccessDeniedException e) {
logger.warn("Update published flag for flow {} forbidden for user {}", id, userDetails.getUsername());
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
}
@PutMapping("/{id}/finalized")
@Operation(summary = "Update finalized flag", description = "Updates the finalized flag of a flow owned by the authenticated user.")
public ResponseEntity<FlowView> updateFinalized(@PathVariable String id,
@RequestBody @Valid FlowFlagUpdateRequest request,
@AuthenticationPrincipal LoginEntity userDetails) {
try {
return ResponseEntity.ok(flowService.updateFinalized(id, userDetails.getUsername(), request));
} catch (FlowNotFoundException e) {
logger.warn("Update finalized flag for flow {} failed for user {}: not found", id, userDetails.getUsername());
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
} catch (FlowAccessDeniedException e) {
logger.warn("Update finalized flag for flow {} forbidden for user {}", id, userDetails.getUsername());
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
}
}

View File

@ -8,6 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest;
import it.cnr.isti.workflow.manager.flows.model.FlowFlagUpdateRequest;
import it.cnr.isti.workflow.manager.flows.model.FlowView;
import it.cnr.isti.workflow.manager.flows.model.FlowViewStatus;
import it.cnr.isti.workflow.manager.flows.repo.FlowEntity;
@ -48,6 +49,7 @@ public class FlowService {
logger.warn("User {} attempted to update flow {} owned by {}", owner, id, entity.getOwner());
throw new FlowAccessDeniedException();
}
ensureMutable(entity);
validateFlow(request);
FlowMapper.updateEntity(entity, request);
@ -63,10 +65,45 @@ public class FlowService {
logger.warn("User {} attempted to delete flow {} owned by {}", owner, id, entity.getOwner());
throw new FlowAccessDeniedException();
}
ensureMutable(entity);
flowRepository.delete(entity);
}
public FlowView updatePublished(String id, String owner, FlowFlagUpdateRequest request) {
FlowEntity entity = flowRepository.findById(id)
.orElseThrow(() -> new FlowNotFoundException(id));
if (!entity.getOwner().equals(owner)) {
logger.warn("User {} attempted to change publish flag of flow {} owned by {}", owner, id, entity.getOwner());
throw new FlowAccessDeniedException();
}
entity.setPublished(Boolean.TRUE.equals(request.value()));
entity.setLastUpdateAt(java.time.LocalDateTime.now());
return toView(flowRepository.save(entity));
}
public FlowView updateFinalized(String id, String owner, FlowFlagUpdateRequest request) {
FlowEntity entity = flowRepository.findById(id)
.orElseThrow(() -> new FlowNotFoundException(id));
if (!entity.getOwner().equals(owner)) {
logger.warn("User {} attempted to change finalized flag of flow {} owned by {}", owner, id, entity.getOwner());
throw new FlowAccessDeniedException();
}
if (entity.isFinalized()) {
if (Boolean.TRUE.equals(request.value())) {
return toView(entity);
}
throw new ResponseStatusException(HttpStatus.CONFLICT, "Flow is finalized");
}
entity.setFinalized(Boolean.TRUE.equals(request.value()));
entity.setLastUpdateAt(java.time.LocalDateTime.now());
return toView(flowRepository.save(entity));
}
public List<FlowView> getAllFlows(String owner) {
return flowRepository.findFlowsByOwnerOrPublic(owner).stream().map(this::toView).toList();
}
@ -96,6 +133,12 @@ public class FlowService {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
}
private void ensureMutable(FlowEntity entity) {
if (entity != null && entity.isFinalized()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Flow is finalized");
}
}
private FlowView toView(FlowEntity entity) {
FlowViewStatus status = flowExecutionValidator.isExecutable(entity.getFlow())
? FlowViewStatus.EXECUTABLE

View File

@ -0,0 +1,7 @@
package it.cnr.isti.workflow.manager.flows.model;
import jakarta.validation.constraints.NotNull;
public record FlowFlagUpdateRequest(
@NotNull Boolean value) {
}

View File

@ -45,6 +45,7 @@ import it.cnr.isti.workflow.manager.flows.model.Connection;
import it.cnr.isti.workflow.manager.flows.model.Flow;
import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest;
import it.cnr.isti.workflow.manager.flows.model.FlowData;
import it.cnr.isti.workflow.manager.flows.model.FlowFlagUpdateRequest;
import it.cnr.isti.workflow.manager.flows.model.FlowView;
import it.cnr.isti.workflow.manager.flows.model.FlowViewStatus;
import it.cnr.isti.workflow.manager.flows.repo.FlowRepository;
@ -336,6 +337,170 @@ public class FlowControllerTest {
assertEquals(404, response.getStatusCode().value());
}
@Test
public void ownerCanUpdatePublishedAndFinalizedFlags() {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
.provider("testProvider")
.model("testModel")
.build();
Flow flow = flowTestCreator.createFlowWithConnection(llmDescriptor);
FlowCreateRequest request = new FlowCreateRequest(
flow.getName(),
flow.getDescription(),
FlowData.builder()
.blocks(flow.getBlocks())
.connections(flow.getConnections())
.build());
ResponseEntity<FlowView> createResponse = flowController.createFlow(
request,
new LoginEntity("testuser", "testpassword"));
assertTrue(createResponse.getStatusCode().is2xxSuccessful());
assertNotNull(createResponse.getBody());
ResponseEntity<FlowView> publishedResponse = flowController.updatePublished(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(true),
new LoginEntity("testuser", "testpassword"));
assertEquals(200, publishedResponse.getStatusCode().value());
assertNotNull(publishedResponse.getBody());
assertTrue(publishedResponse.getBody().published());
ResponseEntity<FlowView> finalizedResponse = flowController.updateFinalized(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(true),
new LoginEntity("testuser", "testpassword"));
assertEquals(200, finalizedResponse.getStatusCode().value());
assertNotNull(finalizedResponse.getBody());
assertTrue(finalizedResponse.getBody().finalized());
}
@Test
public void nonOwnerCannotUpdatePublishedAndFinalizedFlags() {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
.provider("testProvider")
.model("testModel")
.build();
Flow flow = flowTestCreator.createFlowWithConnection(llmDescriptor);
FlowCreateRequest request = new FlowCreateRequest(
flow.getName(),
flow.getDescription(),
FlowData.builder()
.blocks(flow.getBlocks())
.connections(flow.getConnections())
.build());
ResponseEntity<FlowView> createResponse = flowController.createFlow(
request,
new LoginEntity("testuser", "testpassword"));
assertTrue(createResponse.getStatusCode().is2xxSuccessful());
assertNotNull(createResponse.getBody());
ResponseEntity<FlowView> publishedResponse = flowController.updatePublished(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(true),
new LoginEntity("otheruser", "testpassword"));
assertEquals(403, publishedResponse.getStatusCode().value());
ResponseEntity<FlowView> finalizedResponse = flowController.updateFinalized(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(true),
new LoginEntity("otheruser", "testpassword"));
assertEquals(403, finalizedResponse.getStatusCode().value());
}
@Test
public void finalizedFlowCannotBeModified() {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
.provider("testProvider")
.model("testModel")
.build();
Flow flow = flowTestCreator.createFlowWithConnection(llmDescriptor);
FlowCreateRequest createRequest = new FlowCreateRequest(
flow.getName(),
flow.getDescription(),
FlowData.builder()
.blocks(flow.getBlocks())
.connections(flow.getConnections())
.build());
ResponseEntity<FlowView> createResponse = flowController.createFlow(
createRequest,
new LoginEntity("testuser", "testpassword"));
assertTrue(createResponse.getStatusCode().is2xxSuccessful());
assertNotNull(createResponse.getBody());
ResponseEntity<FlowView> finalizedResponse = flowController.updateFinalized(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(true),
new LoginEntity("testuser", "testpassword"));
assertEquals(200, finalizedResponse.getStatusCode().value());
assertTrue(finalizedResponse.getBody().finalized());
Flow updatedFlowDefinition = flowTestCreator.createFlowWithInteraction(llmDescriptor);
FlowCreateRequest updateRequest = new FlowCreateRequest(
updatedFlowDefinition.getName(),
updatedFlowDefinition.getDescription(),
FlowData.builder()
.blocks(updatedFlowDefinition.getBlocks())
.connections(updatedFlowDefinition.getConnections())
.build());
ResponseStatusException updateException = assertThrows(
ResponseStatusException.class,
() -> flowController.updateFlow(
createResponse.getBody().id(),
updateRequest,
new LoginEntity("testuser", "testpassword")));
assertEquals(HttpStatus.CONFLICT, updateException.getStatusCode());
ResponseStatusException publishException = assertThrows(
ResponseStatusException.class,
() -> flowController.updateFinalized(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(false),
new LoginEntity("testuser", "testpassword")));
assertEquals(HttpStatus.CONFLICT, publishException.getStatusCode());
ResponseEntity<FlowView> unpublishedResponse = flowController.updatePublished(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(false),
new LoginEntity("testuser", "testpassword"));
assertEquals(200, unpublishedResponse.getStatusCode().value());
assertNotNull(unpublishedResponse.getBody());
assertTrue(unpublishedResponse.getBody().finalized());
assertEquals(false, unpublishedResponse.getBody().published());
ResponseEntity<FlowView> publishedResponse = flowController.updatePublished(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(true),
new LoginEntity("testuser", "testpassword"));
assertEquals(200, publishedResponse.getStatusCode().value());
assertNotNull(publishedResponse.getBody());
assertTrue(publishedResponse.getBody().finalized());
assertTrue(publishedResponse.getBody().published());
ResponseEntity<FlowView> idempotentFinalizeResponse = flowController.updateFinalized(
createResponse.getBody().id(),
new FlowFlagUpdateRequest(true),
new LoginEntity("testuser", "testpassword"));
assertEquals(200, idempotentFinalizeResponse.getStatusCode().value());
assertTrue(idempotentFinalizeResponse.getBody().finalized());
ResponseStatusException deleteException = assertThrows(
ResponseStatusException.class,
() -> flowController.deleteFlow(
createResponse.getBody().id(),
new LoginEntity("testuser", "testpassword")));
assertEquals(HttpStatus.CONFLICT, deleteException.getStatusCode());
}
@Test
public void deleteFlow() {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()