This is an automated email from the ASF dual-hosted git repository.

zehnder pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new e5bc3a288 [#1936] Adapter Deletion: Disallow Deletion of Pipelines 
Using Adapter but Not Owned by User (#2139)
e5bc3a288 is described below

commit e5bc3a288ddb5da2652db9442327b58b7a2bc04a
Author: Muyang Ye <[email protected]>
AuthorDate: Wed Nov 8 23:22:02 2023 -0800

    [#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 <[email protected]>
---
 .../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 d02e60b69..4980bd993 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.commons.exceptions.connect.AdapterException;
 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.WebApplicationException;
 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<AdapterMasterManagement> {
@@ -164,6 +169,7 @@ public class AdapterResource extends 
AbstractAdapterResource<AdapterMasterManage
                                 @QueryParam("deleteAssociatedPipelines") 
@DefaultValue("false")
                                 boolean deleteAssociatedPipelines) {
     List<String> pipelinesUsingAdapter = getPipelinesUsingAdapter(elementId);
+    IPipelineStorage pipelineStorageAPI = 
StorageDispatcher.INSTANCE.getNoSqlStore().getPipelineStorageAPI();
 
     if (pipelinesUsingAdapter.isEmpty()) {
       try {
@@ -174,25 +180,41 @@ public class AdapterResource extends 
AbstractAdapterResource<AdapterMasterManage
         return ok(Notifications.error(e.getMessage()));
       }
     } else if (!deleteAssociatedPipelines) {
-      List<String> namesOfPipelinesUsingAdapter = new ArrayList<String>();
-      for (String pipelineId : pipelinesUsingAdapter) {
-        namesOfPipelinesUsingAdapter.add(
-            
StorageDispatcher.INSTANCE.getNoSqlStore().getPipelineStorageAPI().getPipeline(pipelineId).getName());
-      }
+      List<String> 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<String> 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 f0d5c511e..c5a73031b 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 @@
 <div class="sp-dialog-container">
     <div class="sp-dialog-content p-15">
         <div
-            *ngIf="!isInProgress && adapterUsedByPipeline"
+            *ngIf="
+                !isInProgress &&
+                adapterUsedByPipeline &&
+                !deleteAssociatedPipelines
+            "
             fxLayout="column"
             style="width: 100%"
         >
@@ -28,9 +32,11 @@
                     <h4>
                         The adapter is currently used by these pipelines:
                         {{ namesOfPipelinesUsingAdapter }}
+                        (if you don't see those pipelines, they are created by
+                        other users)
                     </h4>
                     <h4>
-                        You need to delete these pipelines first before 
deleting
+                        You need to delete those pipelines first before 
deleting
                         the adapter.
                     </h4>
                     <h4>
@@ -51,6 +57,25 @@
                 </button>
             </div>
         </div>
+        <div
+            *ngIf="
+                !isInProgress &&
+                adapterUsedByPipeline &&
+                deleteAssociatedPipelines
+            "
+            fxFlex="100"
+            fxLayoutAlign="center center"
+            fxLayout="column"
+            style="width: 100%"
+        >
+            <b>
+                <h4>
+                    Unable to delete all associated pipelines because you are
+                    not the owner of the following pipelines:
+                    {{ namesOfPipelinesNotOwnedByUser }}
+                </h4>
+            </b>
+        </div>
         <div *ngIf="!isInProgress && !adapterUsedByPipeline" fxLayout="column">
             <div fxFlex="100" fxLayoutAlign="center center" fxLayout="column">
                 <b
diff --git 
a/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.ts
 
b/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.ts
index 7d61e41cf..5ed5b4ed6 100644
--- 
a/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.ts
+++ 
b/ui/src/app/connect/dialog/delete-adapter-dialog/delete-adapter-dialog.component.ts
@@ -35,8 +35,9 @@ export class DeleteAdapterDialogComponent {
     isInProgress = false;
     currentStatus: any;
     adapterUsedByPipeline = false;
+    deleteAssociatedPipelines = false;
     namesOfPipelinesUsingAdapter = '';
-    numberOfPipelinesWithAdapter = 0;
+    namesOfPipelinesNotOwnedByUser = '';
 
     constructor(
         private dialogRef: DialogRef<DeleteAdapterDialogComponent>,
@@ -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;
                     }

Reply via email to