From e5bc3a288ddb5da2652db9442327b58b7a2bc04a Mon Sep 17 00:00:00 2001 From: Muyang Ye Date: Wed, 8 Nov 2023 23:22:02 -0800 Subject: [PATCH] [#1936] Adapter Deletion: Disallow Deletion of Pipelines Using Adapter but Not Owned by User (#2139) * implement new round processor * add English locale, icon, and documentation * fix checkstyle * support different rounding modes * add rounding mode in documentation * fix time display * let NaryMapping selection account for property scope * implement boolean filter unit tests * add common StoreEventCollector class and refactor TestChangedValueDetectionProcessor * add new class * show associated pipelines' names and allow one click deletion * center text * fix minor error * replace magic number * add timeout * restore newline * changeb baseurl * revert port * revert timeout * implement pipelines owner check * undo automatic changes * enable admin to delete pipelines no matter ownership --------- Co-authored-by: bossenti --- .../rest/impl/connect/AdapterResource.java | 56 +++++++++++++------ .../delete-adapter-dialog.component.html | 29 +++++++++- .../delete-adapter-dialog.component.ts | 10 +++- 3 files changed, 74 insertions(+), 21 deletions(-) diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java index d02e60b692..4980bd993f 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java @@ -22,17 +22,22 @@ import org.apache.streampipes.connect.management.management.AdapterMasterManagement; import org.apache.streampipes.connect.management.management.AdapterUpdateManagement; import org.apache.streampipes.manager.pipeline.PipelineManager; +import org.apache.streampipes.model.client.user.Permission; +import org.apache.streampipes.model.client.user.Role; import org.apache.streampipes.model.connect.adapter.AdapterDescription; import org.apache.streampipes.model.message.Notifications; import org.apache.streampipes.model.monitoring.SpLogMessage; +import org.apache.streampipes.resource.management.PermissionResourceManager; import org.apache.streampipes.rest.security.AuthConstants; import org.apache.streampipes.rest.shared.annotation.JacksonSerialized; +import org.apache.streampipes.storage.api.IPipelineStorage; import org.apache.streampipes.storage.management.StorageDispatcher; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DefaultValue; @@ -47,8 +52,8 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @Path("/v2/connect/master/adapters") public class AdapterResource extends AbstractAdapterResource { @@ -164,6 +169,7 @@ public Response deleteAdapter(@PathParam("id") String elementId, @QueryParam("deleteAssociatedPipelines") @DefaultValue("false") boolean deleteAssociatedPipelines) { List pipelinesUsingAdapter = getPipelinesUsingAdapter(elementId); + IPipelineStorage pipelineStorageAPI = StorageDispatcher.INSTANCE.getNoSqlStore().getPipelineStorageAPI(); if (pipelinesUsingAdapter.isEmpty()) { try { @@ -174,25 +180,41 @@ public Response deleteAdapter(@PathParam("id") String elementId, return ok(Notifications.error(e.getMessage())); } } else if (!deleteAssociatedPipelines) { - List namesOfPipelinesUsingAdapter = new ArrayList(); - for (String pipelineId : pipelinesUsingAdapter) { - namesOfPipelinesUsingAdapter.add( - StorageDispatcher.INSTANCE.getNoSqlStore().getPipelineStorageAPI().getPipeline(pipelineId).getName()); - } + List namesOfPipelinesUsingAdapter = + pipelinesUsingAdapter.stream().map(pipelineId -> pipelineStorageAPI.getPipeline(pipelineId).getName()) + .collect( + Collectors.toList()); return Response.status(HttpStatus.SC_CONFLICT).entity(String.join(", ", namesOfPipelinesUsingAdapter)).build(); } else { - try { - // first stop and delete all associated pipelines - for (String pipelineId : pipelinesUsingAdapter) { - PipelineManager.stopPipeline(pipelineId, false); - PipelineManager.deletePipeline(pipelineId); + PermissionResourceManager permissionResourceManager = new PermissionResourceManager(); + // find out the names of pipelines that have an owner and the owner is not the current user + List namesOfPipelinesNotOwnedByUser = pipelinesUsingAdapter.stream().filter(pipelineId -> + !permissionResourceManager.findForObjectId(pipelineId).stream().findFirst().map(Permission::getOwnerSid) + // if a pipeline has no owner, pretend the owner is the user so the user can delete it + .orElse(this.getAuthenticatedUserSid()).equals(this.getAuthenticatedUserSid())) + .map(pipelineId -> pipelineStorageAPI.getPipeline(pipelineId).getName()).collect(Collectors.toList()); + boolean isAdmin = SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream() + .anyMatch(r -> r.getAuthority().equals( + Role.ROLE_ADMIN.name())); + // if the user is admin or owns all pipelines using this adapter, + // the user can delete all associated pipelines and this adapter + if (isAdmin || namesOfPipelinesNotOwnedByUser.isEmpty()) { + try { + for (String pipelineId : pipelinesUsingAdapter) { + PipelineManager.stopPipeline(pipelineId, false); + PipelineManager.deletePipeline(pipelineId); + } + managementService.deleteAdapter(elementId); + return ok(Notifications.success( + "Adapter with id: " + elementId + " and all pipelines using the adapter are deleted.")); + } catch (Exception e) { + LOG.error("Error while deleting adapter with id " + elementId + " and all pipelines using the adapter", e); + return ok(Notifications.error(e.getMessage())); } - managementService.deleteAdapter(elementId); - return ok(Notifications.success( - "Adapter with id: " + elementId + " and all pipelines using the adapter are deleted.")); - } catch (Exception e) { - LOG.error("Error while deleting adapter with id " + elementId + " and all pipelines using the adapter", e); - return ok(Notifications.error(e.getMessage())); + } else { + // otherwise, hint the user the names of pipelines using the adapter but not owned by the user + return Response.status(HttpStatus.SC_CONFLICT).entity(String.join(", ", namesOfPipelinesNotOwnedByUser)) + .build(); } } } diff --git a/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.html b/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.html index f0d5c511e6..c5a73031b4 100644 --- a/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.html +++ b/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.html @@ -19,7 +19,11 @@
@@ -28,9 +32,11 @@

The adapter is currently used by these pipelines: {{ namesOfPipelinesUsingAdapter }} + (if you don't see those pipelines, they are created by + other users)

- You need to delete these pipelines first before deleting + You need to delete those pipelines first before deleting the adapter.

@@ -51,6 +57,25 @@

+
+ +

+ Unable to delete all associated pipelines because you are + not the owner of the following pipelines: + {{ namesOfPipelinesNotOwnedByUser }} +

+
+
, @@ -50,6 +51,7 @@ export class DeleteAdapterDialogComponent { deleteAdapter(deleteAssociatedPipelines: boolean) { this.isInProgress = true; this.currentStatus = 'Deleting adapter...'; + this.deleteAssociatedPipelines = deleteAssociatedPipelines; this.dataMarketplaceService .deleteAdapter(this.adapter, deleteAssociatedPipelines) @@ -59,7 +61,11 @@ export class DeleteAdapterDialogComponent { }, error => { if (error.status === 409) { - this.namesOfPipelinesUsingAdapter = error.error; + if (deleteAssociatedPipelines) { + this.namesOfPipelinesNotOwnedByUser = error.error; + } else { + this.namesOfPipelinesUsingAdapter = error.error; + } this.adapterUsedByPipeline = true; this.isInProgress = false; }