From da66ce29a4f74ad1f1810366f3cdea6101074508 Mon Sep 17 00:00:00 2001 From: Philipp Zehnder Date: Fri, 20 Dec 2024 14:19:38 +0100 Subject: [PATCH] fix(#3372): Fix time range selector test (#3373) * fix(#3372): Try to reproduce ci error * fix(#3372): Only activate one test in GitHub action * fix(#3372): Only activate one test in GitHub action * fix(#3372): get localized time independent of time zone * fix(#3372): Change pr build validation * fix(#3372): Added further changes to the API and provided two more tests * fix(#3372): Remove Adapter Exception from AdapterMasterManagementTest * fix(#3372): Fix to save data view configurations * fix(#3372): Fix delete adapter tests * fix(#3372): Extract method to validate sidebar entries * fix(#3372): WIP share adapter test * fix(#3372): Change tests to share adapters * fix(#3372): Change tests to share pipelines * fix(#3372): Add test for pipeline users * fix(#3372): Fix test testGroupManagement --- .../management/AdapterMasterManagement.java | 18 +-- .../AdapterMasterManagementTest.java | 19 +-- .../streampipes/rest/ResetManagement.java | 50 +++--- .../rest/impl/AdapterMonitoringResource.java | 42 ++++- .../rest/impl/PipelineMonitoring.java | 33 +++- .../rest/impl/PipelineResource.java | 43 +++-- .../impl/connect/AbstractAdapterResource.java | 15 ++ .../rest/impl/connect/AdapterResource.java | 152 +++++++++++------- .../impl/connect/CompactAdapterResource.java | 6 +- .../impl/connect/DescriptionResource.java | 6 + .../rest/impl/connect/GuessResource.java | 12 ++ .../connect/RuntimeResolvableResource.java | 2 + .../rest/impl/connect/UnitResource.java | 7 +- .../rest/security/SpPermissionEvaluator.java | 76 ++++++--- ui/cypress/support/utils/GeneralUtils.ts | 7 + ui/cypress/support/utils/UserUtils.ts | 19 +++ .../support/utils/connect/ConnectUtils.ts | 9 +- .../support/utils/pipeline/PipelineBtns.ts | 8 + .../support/utils/pipeline/PipelineUtils.ts | 39 ++++- .../support/utils/user/PermissionUtils.ts | 44 +++++ .../utils/userInput/StaticPropertyUtils.ts | 10 +- ...leteAdapterWithMultipleUsers.smoke.spec.ts | 75 --------- .../enhancedAdapterDeletion.smoke.spec.ts | 43 +++++ .../tests/datalake/timeRangeSelectors.spec.ts | 5 +- .../multiUser/pipelineMultiUserSupport.ts | 49 ++++++ .../tests/pipeline/pipelineTest.smoke.spec.ts | 23 +-- .../pipeline/updatePipelineTest.smoke.spec.ts | 23 +-- .../testGroupManagement.spec.ts | 16 +- .../testUserRoleConnect.spec.ts | 67 ++++---- .../testUserRolePipeline.spec.ts | 116 ++++++------- .../testVariousUserRoles.smoke.spec.ts | 31 +--- .../sp-table/sp-table.component.html | 6 +- .../existing-adapters.component.html | 1 + .../object-permission-dialog.component.html | 9 +- .../object-permission-dialog.component.ts | 2 +- .../pipeline-overview.component.html | 4 +- 36 files changed, 677 insertions(+), 410 deletions(-) create mode 100644 ui/cypress/support/utils/user/PermissionUtils.ts delete mode 100644 ui/cypress/tests/connect/deleteAdapterWithMultipleUsers.smoke.spec.ts create mode 100644 ui/cypress/tests/connect/enhancedAdapterDeletion.smoke.spec.ts create mode 100644 ui/cypress/tests/pipeline/multiUser/pipelineMultiUserSupport.ts diff --git a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java index 5c42478431..79310b96b1 100644 --- a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java +++ b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterMasterManagement.java @@ -97,7 +97,7 @@ private void createDataStreamForAdapter( var storedDescription = new SourcesManagement() .createAdapterDataStream(adapterDescription, streamId); storedDescription.setCorrespondingAdapterId(adapterId); - installDataSource(storedDescription, principalSid, true); + installDataSource(storedDescription, principalSid); LOG.info("Install source (source URL: {} in backend", adapterDescription.getElementId()); } @@ -141,15 +141,8 @@ public void deleteAdapter(String elementId) throws AdapterException { LOG.info("Successfully deleted data stream: " + adapter.getCorrespondingDataStreamElementId()); } - public List getAllAdapterInstances() throws AdapterException { - - List allAdapters = adapterInstanceStorage.findAll(); - - if (allAdapters == null) { - throw new AdapterException("Could not get all adapters"); - } - - return allAdapters; + public List getAllAdapterInstances() { + return adapterInstanceStorage.findAll(); } public void stopStreamAdapter(String elementId) throws AdapterException { @@ -198,11 +191,10 @@ public void startStreamAdapter(String elementId) throws AdapterException { private void installDataSource( SpDataStream stream, - String principalSid, - boolean publicElement + String principalSid ) throws AdapterException { try { - new DataStreamVerifier(stream).verifyAndAdd(principalSid, publicElement); + new DataStreamVerifier(stream).verifyAndAdd(principalSid, false); } catch (SepaParseException e) { LOG.error("Error while installing data source: {}", stream.getElementId(), e); throw new AdapterException(); diff --git a/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterMasterManagementTest.java b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterMasterManagementTest.java index e1e18324ce..e8095649b8 100644 --- a/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterMasterManagementTest.java +++ b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterMasterManagementTest.java @@ -71,7 +71,7 @@ public void getAdapter_Fail() { } @Test - public void getAllAdapters_Success() throws AdapterException { + public void getAllAdapters_Success() { var adapterDescriptions = List.of(new AdapterDescription()); var adapterStorage = mock(AdapterInstanceStorageImpl.class); var resourceManager = mock(AdapterResourceManager.class); @@ -90,21 +90,4 @@ public void getAllAdapters_Success() throws AdapterException { Assertions.assertEquals(1, result.size()); } - @Test - public void getAllAdapters_Fail() { - var adapterStorage = mock(AdapterInstanceStorageImpl.class); - var resourceManager = mock(AdapterResourceManager.class); - when(adapterStorage.findAll()).thenReturn(null); - - var adapterMasterManagement = - new AdapterMasterManagement( - adapterStorage, - resourceManager, - null, - AdapterMetricsManager.INSTANCE.getAdapterMetrics() - ); - - assertThrows(AdapterException.class, adapterMasterManagement::getAllAdapterInstances); - - } } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java index caca0dac6c..0c88e461b6 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/ResetManagement.java @@ -108,24 +108,20 @@ private static void stopAndDeleteAllPipelines() { private static void stopAndDeleteAllAdapters() { AdapterMasterManagement adapterMasterManagement = new AdapterMasterManagement( StorageDispatcher.INSTANCE.getNoSqlStore() - .getAdapterInstanceStorage(), + .getAdapterInstanceStorage(), new SpResourceManager().manageAdapters(), new SpResourceManager().manageDataStreams(), AdapterMetricsManager.INSTANCE.getAdapterMetrics() ); - try { - List allAdapters = adapterMasterManagement.getAllAdapterInstances(); - allAdapters.forEach(adapterDescription -> { - try { - adapterMasterManagement.deleteAdapter(adapterDescription.getElementId()); - } catch (AdapterException e) { - logger.error("Failed to delete adapter with id: " + adapterDescription.getElementId(), e); - } - }); - } catch (AdapterException e) { - logger.error("Failed to load all adapter descriptions", e); - } + List allAdapters = adapterMasterManagement.getAllAdapterInstances(); + allAdapters.forEach(adapterDescription -> { + try { + adapterMasterManagement.deleteAdapter(adapterDescription.getElementId()); + } catch (AdapterException e) { + logger.error("Failed to delete adapter with id: " + adapterDescription.getElementId(), e); + } + }); } private static void deleteAllFiles() { @@ -154,37 +150,37 @@ private static void removeAllDataInDataLake() { private static void removeAllDataViewWidgets() { var widgetStorage = StorageDispatcher.INSTANCE.getNoSqlStore() - .getDataExplorerWidgetStorage(); + .getDataExplorerWidgetStorage(); widgetStorage.findAll() - .forEach(widget -> widgetStorage.deleteElementById(widget.getElementId())); + .forEach(widget -> widgetStorage.deleteElementById(widget.getElementId())); } private static void removeAllDataViews() { var dataLakeDashboardStorage = StorageDispatcher.INSTANCE.getNoSqlStore() - .getDataExplorerDashboardStorage(); + .getDataExplorerDashboardStorage(); dataLakeDashboardStorage.findAll() - .forEach(dashboard -> dataLakeDashboardStorage.deleteElementById(dashboard.getElementId())); + .forEach(dashboard -> dataLakeDashboardStorage.deleteElementById(dashboard.getElementId())); } private static void removeAllDashboardWidgets() { var dashboardWidgetStorage = StorageDispatcher.INSTANCE.getNoSqlStore() - .getDashboardWidgetStorage(); + .getDashboardWidgetStorage(); dashboardWidgetStorage.findAll() - .forEach(widget -> dashboardWidgetStorage.deleteElementById(widget.getElementId())); + .forEach(widget -> dashboardWidgetStorage.deleteElementById(widget.getElementId())); } private static void removeAllDashboards() { var dashboardStorage = StorageDispatcher.INSTANCE.getNoSqlStore() - .getDashboardStorage(); + .getDashboardStorage(); dashboardStorage.findAll() - .forEach(dashboard -> dashboardStorage.deleteElementById(dashboard.getElementId())); + .forEach(dashboard -> dashboardStorage.deleteElementById(dashboard.getElementId())); } private static void removeAllAssets(String username) { IGenericStorage genericStorage = StorageDispatcher.INSTANCE.getNoSqlStore() - .getGenericStorage(); + .getGenericStorage(); try { for (Map asset : genericStorage.findAll("asset-management")) { genericStorage.delete((String) asset.get("_id"), (String) asset.get("_rev")); @@ -211,13 +207,19 @@ private static void clearGenericStorage() { "asset-management", "asset-sites" ); - var genericStorage = StorageDispatcher.INSTANCE.getNoSqlStore().getGenericStorage(); + var genericStorage = StorageDispatcher.INSTANCE.getNoSqlStore() + .getGenericStorage(); appDocTypesToDelete.forEach(docType -> { try { var allDocs = genericStorage.findAll(docType); for (var doc : allDocs) { - genericStorage.delete(doc.get("_id").toString(), doc.get("_rev").toString()); + genericStorage.delete( + doc.get("_id") + .toString(), + doc.get("_rev") + .toString() + ); } } catch (IOException e) { throw new RuntimeException(e); diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AdapterMonitoringResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AdapterMonitoringResource.java index 8260c4c9c6..0d7363f781 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AdapterMonitoringResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AdapterMonitoringResource.java @@ -21,11 +21,13 @@ import org.apache.streampipes.manager.monitoring.pipeline.ExtensionsLogProvider; import org.apache.streampipes.manager.monitoring.pipeline.ExtensionsServiceLogExecutor; +import org.apache.streampipes.model.client.user.DefaultPrivilege; import org.apache.streampipes.model.monitoring.SpLogEntry; import org.apache.streampipes.model.monitoring.SpMetricsEntry; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,21 +41,51 @@ @RequestMapping("/api/v2/adapter-monitoring") public class AdapterMonitoringResource extends AbstractMonitoringResource { - @GetMapping(path = "adapter/{elementId}/logs", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getLogInfoForAdapter(@PathVariable("elementId") String elementId) { + @GetMapping( + path = "adapter/{elementId}/logs", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @PreAuthorize("this.hasReadAuthority() and hasPermission('#elementId', 'READ')") + public ResponseEntity> getLogInfoForAdapter( + @PathVariable("elementId") String elementId + ) { return ok(ExtensionsLogProvider.INSTANCE.getLogInfosForResource(elementId)); } - @GetMapping(path = "adapter/{elementId}/metrics", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getMetricsInfoForAdapter(@PathVariable("elementId") String elementId) { + @GetMapping( + path = "adapter/{elementId}/metrics", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @PreAuthorize("this.hasReadAuthority() and hasPermission('#elementId', 'READ')") + public ResponseEntity getMetricsInfoForAdapter( + @PathVariable("elementId") String elementId + ) { return ok(ExtensionsLogProvider.INSTANCE.getMetricInfosForResource(elementId)); } - @GetMapping(path = "metrics", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping( + path = "metrics", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @PreAuthorize("this.hasReadAuthority() and hasPermission('#elementId', 'READ')") public ResponseEntity> getMetricsInfos( @RequestParam(value = "filter") List elementIds ) { new ExtensionsServiceLogExecutor().triggerUpdate(); return ok(ExtensionsLogProvider.INSTANCE.getMetricsInfoForResources(elementIds)); } + + /** + * required by Spring expression + */ + public boolean hasReadAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_READ_ADAPTER_VALUE); + } + + /** + * required by Spring expression + */ + public boolean hasWriteAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_WRITE_ADAPTER_VALUE); + } } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineMonitoring.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineMonitoring.java index f772754391..135bf5f938 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineMonitoring.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineMonitoring.java @@ -19,11 +19,13 @@ import org.apache.streampipes.manager.monitoring.pipeline.ExtensionsLogProvider; import org.apache.streampipes.manager.monitoring.pipeline.ExtensionsServiceLogExecutor; +import org.apache.streampipes.model.client.user.DefaultPrivilege; import org.apache.streampipes.model.monitoring.SpLogEntry; import org.apache.streampipes.model.monitoring.SpMetricsEntry; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -37,20 +39,43 @@ @RequestMapping("/api/v2/pipeline-monitoring") public class PipelineMonitoring extends AbstractMonitoringResource { - @GetMapping(value = "/pipeline/{pipelineId}/logs", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping( + value = "/pipeline/{pipelineId}/logs", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @PreAuthorize("this.hasReadAuthority() and hasPermission('#pipelineId', 'READ')") public ResponseEntity>> getLogInfoForPipeline( - @PathVariable("pipelineId") String pipelineId) { + @PathVariable("pipelineId") String pipelineId + ) { return ok(ExtensionsLogProvider.INSTANCE.getLogInfosForPipeline(pipelineId)); } - @GetMapping(value = "/pipeline/{pipelineId}/metrics", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping( + value = "/pipeline/{pipelineId}/metrics", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @PreAuthorize("this.hasReadAuthority() and hasPermission('#pipelineId', 'READ')") public ResponseEntity> getMetricsInfoForPipeline( @PathVariable("pipelineId") String pipelineId, - @RequestParam(value = "forceUpdate", required = false, defaultValue = "false") boolean forceUpdate) { + @RequestParam(value = "forceUpdate", required = false, defaultValue = "false") boolean forceUpdate + ) { if (forceUpdate) { new ExtensionsServiceLogExecutor().triggerUpdate(); } return ok(ExtensionsLogProvider.INSTANCE.getMetricInfosForPipeline(pipelineId)); } + /** + * required by Spring expression + */ + public boolean hasReadAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE); + } + + /** + * required by Spring expression + */ + public boolean hasWriteAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_WRITE_PIPELINE_VALUE); + } } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineResource.java index d2dbdf5676..b897a9d599 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineResource.java @@ -25,6 +25,7 @@ import org.apache.streampipes.manager.pipeline.compact.CompactPipelineManagement; import org.apache.streampipes.manager.recommender.ElementRecommender; import org.apache.streampipes.manager.storage.PipelineStorageService; +import org.apache.streampipes.model.client.user.DefaultPrivilege; import org.apache.streampipes.model.message.ErrorMessage; import org.apache.streampipes.model.message.Message; import org.apache.streampipes.model.message.Notification; @@ -37,7 +38,6 @@ import org.apache.streampipes.model.pipeline.PipelineOperationStatus; import org.apache.streampipes.model.pipeline.compact.CompactPipeline; import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource; -import org.apache.streampipes.rest.security.AuthConstants; import org.apache.streampipes.rest.shared.exception.SpMessageException; import org.apache.streampipes.rest.shared.exception.SpNotificationException; import org.apache.streampipes.storage.management.StorageDispatcher; @@ -95,7 +95,7 @@ public PipelineResource() { mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = Pipeline.class)) )})}) - @PreAuthorize(AuthConstants.HAS_READ_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasReadAuthority()") @PostFilter("hasPermission(filterObject.pipelineId, 'READ')") public List get() { return PipelineManager.getAllPipelines(); @@ -105,7 +105,7 @@ public List get() { path = "{pipelineId}/status", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Get the pipeline status of a given pipeline", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_READ_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasReadAuthority()") public List getPipelineStatus(@PathVariable("pipelineId") String pipelineId) { return PipelineStatusManager.getPipelineStatus(pipelineId, 5); } @@ -114,15 +114,15 @@ public List getPipelineStatus(@PathVariable("pipelineId") path = "/{pipelineId}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Delete a pipeline with a given id", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) - public Message removeOwn(@PathVariable("pipelineId") String pipelineId) { + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#pipelineId', 'WRITE')") + public Message delete(@PathVariable("pipelineId") String pipelineId) { PipelineManager.deletePipeline(pipelineId); return Notifications.success("Pipeline deleted"); } @GetMapping(path = "/{pipelineId}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Get a specific pipeline with the given id", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_READ_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasReadAuthority() and hasPermission('#pipelineId', 'READ')") public ResponseEntity getElement(@PathVariable("pipelineId") String pipelineId) { Pipeline foundPipeline = PipelineManager.getPipeline(pipelineId); @@ -135,7 +135,7 @@ public ResponseEntity getElement(@PathVariable("pipelineId") String pi @GetMapping(path = "/{pipelineId}/start", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Start the pipeline with the given id", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#pipelineId', 'WRITE')") public ResponseEntity start(@PathVariable("pipelineId") String pipelineId) { try { PipelineOperationStatus status = PipelineManager.startPipeline(pipelineId); @@ -149,7 +149,7 @@ public ResponseEntity start(@PathVariable("pipelineId") String pipelineId) { @GetMapping(path = "/{pipelineId}/stop", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Stop the pipeline with the given id", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#pipelineId', 'WRITE')") public ResponseEntity stop(@PathVariable("pipelineId") String pipelineId, @RequestParam(value = "forceStop", defaultValue = "false") boolean forceStop) { try { @@ -166,7 +166,7 @@ public ResponseEntity stop(@PathVariable("pipelineId") String pipelineId, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Store a new pipeline", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity addPipeline(@RequestBody Pipeline pipeline) { String pipelineId = PipelineManager.addPipeline(getAuthenticatedUserSid(), pipeline); @@ -180,7 +180,7 @@ public ResponseEntity addPipeline(@RequestBody Pipeline pipeline consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Convert a pipeline to the compact model", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity convertToCompactPipeline(@RequestBody Pipeline pipeline) { return ok(compactPipelineManagement.convertPipeline(pipeline)); } @@ -190,7 +190,7 @@ public ResponseEntity convertToCompactPipeline(@RequestBody Pip consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Hidden - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority()") @PostAuthorize("hasPermission(returnObject, 'READ')") public PipelineElementRecommendationMessage recommend(@RequestBody Pipeline pipeline, @PathVariable("recId") String baseRecElement) { @@ -221,7 +221,7 @@ public PipelineElementRecommendationMessage recommend(@RequestBody Pipeline pipe consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Hidden - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity validatePipeline(@RequestBody Pipeline pipeline) { try { return ok(new PipelineVerificationHandlerV2(pipeline).verifyPipeline()); @@ -238,8 +238,8 @@ public ResponseEntity validatePipeline(@RequestBody Pipeline pipeline) { produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Update an existing pipeline", tags = {"Pipeline"}) - @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) - public ResponseEntity overwritePipeline(@PathVariable("pipelineId") String pipelineId, + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#pipelineId', 'WRITE')") + public ResponseEntity updatePipeline(@PathVariable("pipelineId") String pipelineId, @RequestBody Pipeline pipeline) { Pipeline storedPipeline = getPipelineStorage().getElementById(pipelineId); if (!storedPipeline.isRunning()) { @@ -264,10 +264,23 @@ public ResponseEntity overwritePipeline(@PathVariable("pipelineI "Pipeline"}, responses = {@ApiResponse(content = { @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = Pipeline.class)))})}) - @PreAuthorize(AuthConstants.HAS_READ_PIPELINE_PRIVILEGE) + @PreAuthorize("this.hasReadAuthority()") @PostFilter("hasPermission(filterObject.pipelineId, 'READ')") public List getPipelinesContainingElement(@PathVariable("elementId") String elementId) { return PipelineManager.getPipelinesContainingElements(elementId); } + /** + * required by Spring expression + */ + public boolean hasReadAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE); + } + + /** + * required by Spring expression + */ + public boolean hasWriteAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_WRITE_PIPELINE_VALUE); + } } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AbstractAdapterResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AbstractAdapterResource.java index c9623a4df1..dcdd800b6a 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AbstractAdapterResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AbstractAdapterResource.java @@ -17,6 +17,7 @@ */ package org.apache.streampipes.rest.impl.connect; +import org.apache.streampipes.model.client.user.DefaultPrivilege; import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource; import java.util.function.Supplier; @@ -28,4 +29,18 @@ public class AbstractAdapterResource extends AbstractAuthGuardedRestResource public AbstractAdapterResource(Supplier managementServiceSupplier) { this.managementService = managementServiceSupplier.get(); } + + /** + * required by Spring expression + */ + public boolean hasReadAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_READ_ADAPTER_VALUE); + } + + /** + * required by Spring expression + */ + public boolean hasWriteAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_WRITE_ADAPTER_VALUE); + } } 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 372769a792..b0a7b2c8e6 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 @@ -36,6 +36,7 @@ import org.apache.streampipes.resource.management.PermissionResourceManager; import org.apache.streampipes.resource.management.SpResourceManager; import org.apache.streampipes.rest.security.AuthConstants; +import org.apache.streampipes.rest.security.SpPermissionEvaluator; import org.apache.streampipes.rest.shared.constants.SpMediaType; import org.apache.streampipes.storage.api.IPipelineStorage; import org.apache.streampipes.storage.management.StorageDispatcher; @@ -46,6 +47,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.DeleteMapping; @@ -70,7 +72,7 @@ public class AdapterResource extends AbstractAdapterResource new AdapterMasterManagement( StorageDispatcher.INSTANCE.getNoSqlStore() - .getAdapterInstanceStorage(), + .getAdapterInstanceStorage(), new SpResourceManager().manageAdapters(), new SpResourceManager().manageDataStreams(), AdapterMetricsManager.INSTANCE.getAdapterMetrics() @@ -78,7 +80,7 @@ public AdapterResource() { } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity addAdapter(@RequestBody AdapterDescription adapterDescription) { var principalSid = getAuthenticatedUserSid(); var username = getAuthenticatedUsername(); @@ -102,14 +104,14 @@ public ResponseEntity addAdapter(@RequestBody AdapterDescript SpMediaType.YAML, SpMediaType.YML }) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity convertToCompactAdapter(@RequestBody AdapterDescription adapterDescription) throws Exception { return ok(new CompactAdapterManagement(List.of()).convertToCompactAdapter(adapterDescription)); } @PutMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#adapterDescription.elementId', 'WRITE')") public ResponseEntity updateAdapter(@RequestBody AdapterDescription adapterDescription) { var updateManager = new AdapterUpdateManagement(managementService); try { @@ -123,7 +125,7 @@ public ResponseEntity updateAdapter(@RequestBody AdapterDescr } @PutMapping(path = "pipeline-migration-preflight", consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) public ResponseEntity> performPipelineMigrationPreflight( @RequestBody AdapterDescription adapterDescription @@ -135,60 +137,87 @@ public ResponseEntity> performPipelineMigrationPrefligh } @GetMapping(path = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE, SpMediaType.YAML, SpMediaType.YML}) - @PreAuthorize(AuthConstants.HAS_READ_ADAPTER_PRIVILEGE) - public ResponseEntity getAdapter(@PathVariable("id") String adapterId, - @RequestParam(value = "output", - defaultValue = "full", - required = false) String outputMode) { + @PreAuthorize("this.hasReadAuthority()") + public ResponseEntity getAdapter( + @PathVariable("id") String elementId, + @RequestParam(value = "output", + defaultValue = "full", + required = false) String outputMode + ) { try { - AdapterDescription adapterDescription = getAdapterDescription(adapterId); + var adapterDescription = getAdapterDescription(elementId); + + // This check is done here because the adapter permission is checked based on the corresponding data stream + // and not based on the element id + if (!checkAdapterReadPermission(adapterDescription)) { + LOG.error("User is not allowed to read adapter {}", elementId); + return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED) + .build(); + } + if (outputMode.equalsIgnoreCase("compact")) { return ok(toCompactAdapterDescription(adapterDescription)); } else { return ok(adapterDescription); } } catch (AdapterException e) { - LOG.error("Error while getting adapter with id {}", adapterId, e); + LOG.error("Error while getting adapter with id {}", elementId, e); return fail(); } catch (Exception e) { - LOG.error("Error while transforming adapter {}", adapterId, e); + LOG.error("Error while transforming adapter {}", elementId, e); return fail(); } } + /** + * Checks if the current user has the permission to read the adapter + */ + private boolean checkAdapterReadPermission(AdapterDescription adapterDescription) { + var spPermissionEvaluator = new SpPermissionEvaluator(); + var authentication = SecurityContextHolder.getContext() + .getAuthentication(); + return spPermissionEvaluator.hasPermission( + authentication, + adapterDescription.getCorrespondingDataStreamElementId(), + "READ" + ); + } + @PostMapping(path = "/{id}/stop", produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) - public ResponseEntity stopAdapter(@PathVariable("id") String adapterId) { + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#elementId', 'WRITE')") + public ResponseEntity stopAdapter(@PathVariable("id") String elementId) { try { - managementService.stopStreamAdapter(adapterId); + managementService.stopStreamAdapter(elementId); return ok(Notifications.success("Adapter started")); } catch (AdapterException e) { - LOG.error("Could not stop adapter with id {}", adapterId, e); + LOG.error("Could not stop adapter with id {}", elementId, e); return serverError(SpLogMessage.from(e)); } } @PostMapping(path = "/{id}/start", produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) - public ResponseEntity startAdapter(@PathVariable("id") String adapterId) { + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#elementId', 'WRITE')") + public ResponseEntity startAdapter(@PathVariable("id") String elementId) { try { - managementService.startStreamAdapter(adapterId); + managementService.startStreamAdapter(elementId); return ok(Notifications.success("Adapter stopped")); } catch (AdapterException e) { - LOG.error("Could not start adapter with id {}", adapterId, e); + LOG.error("Could not start adapter with id {}", elementId, e); return serverError(SpLogMessage.from(e)); } } @DeleteMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) - public ResponseEntity deleteAdapter(@PathVariable("id") String elementId, - @RequestParam(value = "deleteAssociatedPipelines", defaultValue = "false") - boolean deleteAssociatedPipelines) { + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#elementId', 'WRITE')") + public ResponseEntity deleteAdapter( + @PathVariable("id") String elementId, + @RequestParam(value = "deleteAssociatedPipelines", defaultValue = "false") + boolean deleteAssociatedPipelines + ) { List pipelinesUsingAdapter = getPipelinesUsingAdapter(elementId); IPipelineStorage pipelineStorageAPI = StorageDispatcher.INSTANCE.getNoSqlStore() - .getPipelineStorageAPI(); + .getPipelineStorageAPI(); if (pipelinesUsingAdapter.isEmpty()) { try { @@ -202,22 +231,40 @@ public ResponseEntity deleteAdapter(@PathVariable("id") String elementId, List namesOfPipelinesUsingAdapter = pipelinesUsingAdapter .stream() .map(pipelineId -> pipelineStorageAPI.getElementById( - pipelineId) - .getName()) + pipelineId) + .getName()) .collect(Collectors.toList()); return ResponseEntity.status(HttpStatus.SC_CONFLICT) - .body(String.join(", ", namesOfPipelinesUsingAdapter)); + .body(String.join(", ", namesOfPipelinesUsingAdapter)); } else { 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.getElementById(pipelineId).getName()).collect(Collectors.toList()); - boolean isAdmin = SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream() - .anyMatch(r -> r.getAuthority().equals( - DefaultRole.ROLE_ADMIN.name())); + 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.getElementById( + pipelineId) + .getName()) + .collect(Collectors.toList()); + boolean isAdmin = SecurityContextHolder.getContext() + .getAuthentication() + .getAuthorities() + .stream() + .anyMatch(r -> r.getAuthority() + .equals( + DefaultRole.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()) { @@ -228,34 +275,31 @@ public ResponseEntity deleteAdapter(@PathVariable("id") String elementId, } managementService.deleteAdapter(elementId); return ok(Notifications.success("Adapter with id: " + elementId - + " and all pipelines using the adapter are deleted.")); + + " 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); + 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 ResponseEntity.status(HttpStatus.SC_CONFLICT) - .body(String.join(", ", namesOfPipelinesNotOwnedByUser)); + .body(String.join(", ", namesOfPipelinesNotOwnedByUser)); } } } @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize(AuthConstants.HAS_READ_ADAPTER_PRIVILEGE) - public ResponseEntity getAllAdapters() { - try { - return ok(managementService.getAllAdapterInstances()); - } catch (AdapterException e) { - LOG.error("Error while getting all adapters", e); - return ResponseEntity.status(500) - .build(); - } + @PreAuthorize("this.hasReadAuthority()") + @PostFilter("hasPermission(filterObject.correspondingDataStreamElementId, 'READ')") + public List getAllAdapters() { + return managementService.getAllAdapterInstances(); } - private AdapterDescription getAdapterDescription(String adapterId) throws AdapterException { - return managementService.getAdapter(adapterId); + private AdapterDescription getAdapterDescription(String elementId) throws AdapterException { + return managementService.getAdapter(elementId); } private CompactAdapter toCompactAdapterDescription(AdapterDescription adapterDescription) throws Exception { @@ -264,8 +308,8 @@ private CompactAdapter toCompactAdapterDescription(AdapterDescription adapterDes private List getPipelinesUsingAdapter(String adapterId) { return StorageDispatcher.INSTANCE.getNoSqlStore() - .getPipelineStorageAPI() - .getPipelinesUsingAdapter(adapterId); + .getPipelineStorageAPI() + .getPipelinesUsingAdapter(adapterId); } } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java index 0e51f00927..89912db3d8 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java @@ -30,7 +30,6 @@ import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter; import org.apache.streampipes.model.message.Notifications; import org.apache.streampipes.resource.management.SpResourceManager; -import org.apache.streampipes.rest.security.AuthConstants; import org.apache.streampipes.rest.shared.constants.SpMediaType; import org.apache.streampipes.rest.shared.exception.BadRequestException; import org.apache.streampipes.storage.management.StorageDispatcher; @@ -75,7 +74,7 @@ public CompactAdapterResource() { SpMediaType.YAML } ) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity addAdapterCompact( @RequestBody CompactAdapter compactAdapter ) throws Exception { @@ -129,7 +128,7 @@ public ResponseEntity addAdapterCompact( "application/yml" } ) - @PreAuthorize(AuthConstants.HAS_WRITE_ADAPTER_PRIVILEGE) + @PreAuthorize("this.hasWriteAuthority() and hasPermission('#elementId', 'WRITE')") public ResponseEntity updateAdapterCompact( @PathVariable("id") String elementId, @RequestBody CompactAdapter compactAdapter @@ -164,4 +163,5 @@ private AdapterDescription getGeneratedAdapterDescription( var generators = adapterGenerationSteps.getGenerators(); return new CompactAdapterManagement(generators).convertToAdapterDescription(compactAdapter, existingAdapter); } + } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/DescriptionResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/DescriptionResource.java index 7d8f562034..d26eaf951a 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/DescriptionResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/DescriptionResource.java @@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -53,6 +54,7 @@ public DescriptionResource() { } @GetMapping(path = "/adapters", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("this.hasReadAuthority()") public ResponseEntity> getAdapters() { List result = managementService.getAdapters(); @@ -60,6 +62,7 @@ public ResponseEntity> getAdapters() { } @GetMapping(path = "/{id}/assets", produces = "application/zip") + @PreAuthorize("this.hasReadAuthority()") public ResponseEntity getAdapterAssets(@PathVariable("id") String id) { try { String result = null; @@ -87,6 +90,7 @@ public ResponseEntity getAdapterAssets(@PathVariable("id") String id) { } @GetMapping(path = "/{id}/assets/icon", produces = "image/png") + @PreAuthorize("this.hasReadAuthority()") public ResponseEntity getAdapterIconAsset(@PathVariable("id") String id) { try { @@ -115,6 +119,7 @@ public ResponseEntity getAdapterIconAsset(@PathVariable("id") String id) { } @GetMapping(path = "/{id}/assets/documentation", produces = MediaType.TEXT_PLAIN_VALUE) + @PreAuthorize("this.hasReadAuthority()") public ResponseEntity getAdapterDocumentationAsset(@PathVariable("id") String id) { try { String result = null; @@ -142,6 +147,7 @@ public ResponseEntity getAdapterDocumentationAsset(@PathVariable("id") String } @DeleteMapping(path = "{adapterId}") + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity deleteAdapter(@PathVariable("adapterId") String adapterId) { try { this.managementService.deleteAdapterDescription(adapterId); diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/GuessResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/GuessResource.java index ddb51a9a92..f49dfaa7c9 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/GuessResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/GuessResource.java @@ -22,6 +22,7 @@ import org.apache.streampipes.commons.exceptions.connect.ParseException; import org.apache.streampipes.connect.management.management.GuessManagement; import org.apache.streampipes.extensions.api.connect.exception.WorkerAdapterException; +import org.apache.streampipes.model.client.user.DefaultPrivilege; import org.apache.streampipes.model.connect.adapter.AdapterDescription; import org.apache.streampipes.model.connect.guess.AdapterEventPreview; import org.apache.streampipes.model.connect.guess.GuessSchema; @@ -32,6 +33,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -53,6 +55,7 @@ public GuessResource() { path = "/schema", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity guessSchema(@RequestBody AdapterDescription adapterDescription) { try { @@ -74,6 +77,7 @@ public ResponseEntity guessSchema(@RequestBody AdapterDescription adapterDesc path = "/schema/preview", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity getAdapterEventPreview(@RequestBody AdapterEventPreview previewRequest) { try { return ok(managementService.performAdapterEventPreview(previewRequest)); @@ -81,5 +85,13 @@ public ResponseEntity getAdapterEventPreview(@RequestBody AdapterEventPreview return badRequest(); } } + + /** + * required by Spring expression + */ + public boolean hasWriteAuthority() { + return isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_WRITE_ADAPTER_VALUE); + } + } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/RuntimeResolvableResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/RuntimeResolvableResource.java index ce66dbeed8..cf30a41758 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/RuntimeResolvableResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/RuntimeResolvableResource.java @@ -38,6 +38,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -67,6 +68,7 @@ public RuntimeResolvableResource() { path = "{id}/configurations", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("this.hasWriteAuthority()") public ResponseEntity fetchConfigurations(@PathVariable("id") String appId, @RequestBody RuntimeOptionsRequest runtimeOptionsRequest) { diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/UnitResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/UnitResource.java index 1efb585e82..db2edcdd6c 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/UnitResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/UnitResource.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -48,6 +49,7 @@ public UnitResource() { consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE ) + @PreAuthorize("this.hasReadAuthority()") public ResponseEntity getFittingUnits(@RequestBody UnitDescription unitDescription) { try { String resultingJson = managementService.getFittingUnits(unitDescription); @@ -59,8 +61,9 @@ public ResponseEntity getFittingUnits(@RequestBody UnitDescription unitDescri } @GetMapping(path = "/units", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getAllUnits(){ + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("this.hasReadAuthority()") + public ResponseEntity> getAllUnits() { List unitDescriptions = managementService.getAllUnitDescriptions(); return ok(unitDescriptions); } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java index 08ee86cc46..86c0a74d80 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/SpPermissionEvaluator.java @@ -35,39 +35,71 @@ @Configuration public class SpPermissionEvaluator implements PermissionEvaluator { + /** + * Evaluates whether the user has the necessary permissions for a given resource. + * + * @param authentication The authentication object containing the user's credentials. + * @param targetDomainObject The resource being accessed, which can be an instance of + * PipelineElementRecommendationMessage or a String representing the resource ID. + * @param permission Is not used in this implementation. + * @return true if the user has the necessary permissions, false otherwise. + */ @Override - public boolean hasPermission(Authentication auth, Object o, Object permission) { - PrincipalUserDetails userDetails = getUserDetails(auth); - if (o instanceof PipelineElementRecommendationMessage) { - return isAdmin(userDetails) || filterRecommendation(auth, (PipelineElementRecommendationMessage) o); + public boolean hasPermission( + Authentication authentication, + Object targetDomainObject, + Object permission + ) { + PrincipalUserDetails userDetails = getUserDetails(authentication); + if (targetDomainObject instanceof PipelineElementRecommendationMessage) { + return isAdmin(userDetails) || filterRecommendation( + authentication, + (PipelineElementRecommendationMessage) targetDomainObject + ); } else { - String objectInstanceId = (String) o; + String objectInstanceId = (String) targetDomainObject; if (isAdmin(userDetails)) { return true; } - return hasPermission(auth, objectInstanceId); + return hasPermission(authentication, objectInstanceId); } } - private boolean filterRecommendation(Authentication auth, PipelineElementRecommendationMessage message) { - Predicate isForbidden = r -> !hasPermission(auth, r.getElementId()); - message.getPossibleElements().removeIf(isForbidden); - - return true; - } - + /** + * Evaluates whether the user has the necessary permissions for a given resource. + * + * @param authentication The authentication object containing the user's credentials. + * @param targetId The ID of the resource being accessed. + * @param targetType Is not used in this implementation. + * @param permission Is not used in this implementation. + * @return true if the user has the necessary permissions, false otherwise. + */ @Override - public boolean hasPermission(Authentication auth, Serializable serializable, String s, Object permission) { - PrincipalUserDetails userDetails = getUserDetails(auth); + public boolean hasPermission( + Authentication authentication, + Serializable targetId, + String targetType, + Object permission + ) { + PrincipalUserDetails userDetails = getUserDetails(authentication); if (isAdmin(userDetails)) { return true; } - return hasPermission(auth, serializable.toString()); + return hasPermission(authentication, targetId.toString()); + } + + private boolean filterRecommendation(Authentication auth, PipelineElementRecommendationMessage message) { + Predicate isForbidden = r -> !hasPermission(auth, r.getElementId()); + message.getPossibleElements() + .removeIf(isForbidden); + + return true; } private boolean hasPermission(Authentication auth, String objectInstanceId) { return isPublicElement(objectInstanceId) - || getUserDetails(auth).getAllObjectPermissions().contains(objectInstanceId); + || getUserDetails(auth).getAllObjectPermissions() + .contains(objectInstanceId); } private PrincipalUserDetails getUserDetails(Authentication authentication) { @@ -76,14 +108,18 @@ private PrincipalUserDetails getUserDetails(Authentication authentication) { private boolean isPublicElement(String objectInstanceId) { List permissions = - StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage().getUserPermissionsForObject(objectInstanceId); - return permissions.size() > 0 && permissions.get(0).isPublicElement(); + StorageDispatcher.INSTANCE.getNoSqlStore() + .getPermissionStorage() + .getUserPermissionsForObject(objectInstanceId); + return permissions.size() > 0 && permissions.get(0) + .isPublicElement(); } private boolean isAdmin(PrincipalUserDetails userDetails) { return userDetails .getAuthorities() .stream() - .anyMatch(a -> a.getAuthority().equals(DefaultRole.Constants.ROLE_ADMIN_VALUE)); + .anyMatch(a -> a.getAuthority() + .equals(DefaultRole.Constants.ROLE_ADMIN_VALUE)); } } diff --git a/ui/cypress/support/utils/GeneralUtils.ts b/ui/cypress/support/utils/GeneralUtils.ts index 8db505e32b..9b0186b1f6 100644 --- a/ui/cypress/support/utils/GeneralUtils.ts +++ b/ui/cypress/support/utils/GeneralUtils.ts @@ -20,4 +20,11 @@ export class GeneralUtils { public static tab(identifier: string) { return cy.dataCy(`tab-${identifier}`).click(); } + + public static validateAmountOfNavigationIcons(expected: number) { + cy.dataCy('navigation-icon', { timeout: 10000 }).should( + 'have.length', + expected, + ); + } } diff --git a/ui/cypress/support/utils/UserUtils.ts b/ui/cypress/support/utils/UserUtils.ts index b91f046f07..3f966450db 100644 --- a/ui/cypress/support/utils/UserUtils.ts +++ b/ui/cypress/support/utils/UserUtils.ts @@ -62,6 +62,25 @@ export class UserUtils { cy.dataCy('sp-element-edit-user-save').click(); } + /** + * Create a new user with the specified roles and a default password to the system. + * + * @param name - The name of the user to be added. + * @param roles - The roles to be assigned to the new user. + */ + public static createUser(name: string, ...roles: UserRole[]): User { + const userBuilder = UserBuilder.create(`${name}@streampipes.apache.org`) + .setName(name) + .setPassword('default'); + + roles.forEach(role => userBuilder.addRole(role)); + + const user = userBuilder.build(); + + this.addUser(user); + return user; + } + public static switchUser(user: User) { cy.logout(); cy.visit('#/login'); diff --git a/ui/cypress/support/utils/connect/ConnectUtils.ts b/ui/cypress/support/utils/connect/ConnectUtils.ts index 0a0eae22c1..52d2ec6c88 100644 --- a/ui/cypress/support/utils/connect/ConnectUtils.ts +++ b/ui/cypress/support/utils/connect/ConnectUtils.ts @@ -440,6 +440,13 @@ export class ConnectUtils { public static checkAmountOfAdapters(amount: number) { ConnectUtils.goToConnect(); - ConnectBtns.deleteAdapter().should('have.length', amount); + if (amount === 0) { + // The wait is needed because the default value is the no-table-entries element. + // It must be waited till the data is loaded. Once a better solution is found, this can be removed. + cy.wait(1000); + cy.dataCy('no-table-entries').should('be.visible'); + } else { + ConnectBtns.deleteAdapter().should('have.length', amount); + } } } diff --git a/ui/cypress/support/utils/pipeline/PipelineBtns.ts b/ui/cypress/support/utils/pipeline/PipelineBtns.ts index 6ace82220f..faece471a5 100644 --- a/ui/cypress/support/utils/pipeline/PipelineBtns.ts +++ b/ui/cypress/support/utils/pipeline/PipelineBtns.ts @@ -17,6 +17,14 @@ */ export class PipelineBtns { + public static statusPipeline() { + return cy.dataCy('status-pipeline-green', { timeout: 10000 }); + } + + public static stopPipeline() { + return cy.dataCy('stop-pipeline-button', { timeout: 10000 }); + } + public static deletePipeline() { return cy.dataCy('delete-pipeline', { timeout: 10000 }); } diff --git a/ui/cypress/support/utils/pipeline/PipelineUtils.ts b/ui/cypress/support/utils/pipeline/PipelineUtils.ts index 0fa9ce4443..2b50f512c0 100644 --- a/ui/cypress/support/utils/pipeline/PipelineUtils.ts +++ b/ui/cypress/support/utils/pipeline/PipelineUtils.ts @@ -21,6 +21,9 @@ import { StaticPropertyUtils } from '../userInput/StaticPropertyUtils'; import { OutputStrategyUtils } from '../OutputStrategyUtils'; import { PipelineElementInput } from '../../model/PipelineElementInput'; import { PipelineBtns } from './PipelineBtns'; +import { ConnectUtils } from '../connect/ConnectUtils'; +import { PipelineBuilder } from '../../builder/PipelineBuilder'; +import { PipelineElementBuilder } from '../../builder/PipelineElementBuilder'; export class PipelineUtils { public static addPipeline(pipelineInput: PipelineInput) { @@ -33,6 +36,32 @@ export class PipelineUtils { PipelineUtils.startPipeline(pipelineInput); } + /** + * This method adds a sample adapter and pipeline + */ + public static addSampleAdapterAndPipeline() { + const adapterName = 'simulator'; + + ConnectUtils.addMachineDataSimulator(adapterName); + + const pipelineInput = PipelineBuilder.create('Pipeline Test') + .addSource(adapterName) + .addProcessingElement( + PipelineElementBuilder.create('field_renamer') + .addInput('drop-down', 'convert-property', 'timestamp') + .addInput('input', 'field-name', 't') + .build(), + ) + .addSink( + PipelineElementBuilder.create('data_lake') + .addInput('input', 'db_measurement', 'demo') + .build(), + ) + .build(); + + PipelineUtils.addPipeline(pipelineInput); + } + public static editPipeline() { cy.dataCy('modify-pipeline-btn').first().click(); } @@ -133,7 +162,15 @@ export class PipelineUtils { public static checkAmountOfPipelinesPipeline(amount: number) { PipelineUtils.goToPipelines(); - PipelineBtns.deletePipeline().should('have.length', amount); + + if (amount === 0) { + // The wait is needed because the default value is the no-table-entries element. + // It must be waited till the data is loaded. Once a better solution is found, this can be removed. + cy.wait(1000); + cy.dataCy('no-table-entries').should('be.visible'); + } else { + PipelineBtns.statusPipeline().should('have.length', amount); + } } public static deletePipeline() { diff --git a/ui/cypress/support/utils/user/PermissionUtils.ts b/ui/cypress/support/utils/user/PermissionUtils.ts new file mode 100644 index 0000000000..9907c56acc --- /dev/null +++ b/ui/cypress/support/utils/user/PermissionUtils.ts @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { StaticPropertyUtils } from '../userInput/StaticPropertyUtils'; + +export class PermissionUtils { + public static openManagePermissions() { + cy.dataCy('open-manage-permissions').click(); + } + + public static markElementAsPublic() { + PermissionUtils.openManagePermissions(); + StaticPropertyUtils.clickCheckbox('permission-public-element'); + PermissionUtils.save(); + } + + public static authorizeUser(email: string) { + PermissionUtils.openManagePermissions(); + + cy.dataCy('authorized-user').type(email); + cy.get(`[data-cy="user-option-${email}"]`).click(); + + PermissionUtils.save(); + } + + public static save() { + cy.dataCy('sp-manage-permissions-save').click(); + } +} diff --git a/ui/cypress/support/utils/userInput/StaticPropertyUtils.ts b/ui/cypress/support/utils/userInput/StaticPropertyUtils.ts index 34b84673f3..acc1a5eaf4 100644 --- a/ui/cypress/support/utils/userInput/StaticPropertyUtils.ts +++ b/ui/cypress/support/utils/userInput/StaticPropertyUtils.ts @@ -24,7 +24,7 @@ export class StaticPropertyUtils { // Configure Properties configs.forEach(config => { if (config.type === 'checkbox') { - this.clickCheckbox(config); + this.clickCheckbox(config.selector); } else if (config.type === 'button') { cy.dataCy(config.selector, { timeout: 2000 }).click(); } else if (config.type === 'drop-down') { @@ -65,8 +65,12 @@ export class StaticPropertyUtils { }); } - private static clickCheckbox(input: UserInput) { - this.clickSelectionInput(input.selector, '.mdc-checkbox'); + /** + * This method can be used to check a mat checkbox + * @param selector + */ + public static clickCheckbox(selector: string) { + this.clickSelectionInput(selector, '.mdc-checkbox'); } private static clickRadio(input: UserInput) { diff --git a/ui/cypress/tests/connect/deleteAdapterWithMultipleUsers.smoke.spec.ts b/ui/cypress/tests/connect/deleteAdapterWithMultipleUsers.smoke.spec.ts deleted file mode 100644 index a11332ac9e..0000000000 --- a/ui/cypress/tests/connect/deleteAdapterWithMultipleUsers.smoke.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; -import { PipelineBuilder } from '../../support/builder/PipelineBuilder'; -import { PipelineElementBuilder } from '../../support/builder/PipelineElementBuilder'; -import { UserUtils } from '../../support/utils/UserUtils'; -import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils'; - -const adapterName = 'simulator'; - -const pipelineInput = PipelineBuilder.create('Pipeline Test') - .addSource(adapterName) - .addProcessingElement( - PipelineElementBuilder.create('field_renamer') - .addInput('drop-down', 'convert-property', 'timestamp') - .addInput('input', 'field-name', 't') - .build(), - ) - .addSink( - PipelineElementBuilder.create('data_lake') - .addInput('input', 'db_measurement', 'demo') - .build(), - ) - .build(); - -describe('Test Enhanced Adapter Deletion', () => { - beforeEach('Setup Test', () => { - cy.initStreamPipesTest(); - UserUtils.addUser(UserUtils.userWithAdapterAndPipelineAdminRights); - }); - - it('Test Delete Adapter and Associated Pipelines', () => { - ConnectUtils.addMachineDataSimulator(adapterName, false); - PipelineUtils.addPipeline(pipelineInput); - - ConnectUtils.deleteAdapterAndAssociatedPipelines(); - }); - - it('Test Admin Should Be Able to Delete Adapter and Not Owned Associated Pipelines', () => { - // Let the user create the adapter and the pipeline - ConnectUtils.addMachineDataSimulator(adapterName, false); - PipelineUtils.addPipeline(pipelineInput); - - // Then let the admin delete them - UserUtils.switchUser(UserUtils.adminUser); - ConnectUtils.deleteAdapterAndAssociatedPipelines(true); - }); - - it('Test Delete Adapter and Associated Pipelines Permission Denied', () => { - // Let the admin create the adapter and the pipeline - UserUtils.switchUser(UserUtils.adminUser); - ConnectUtils.addMachineDataSimulator(adapterName, false); - PipelineUtils.addPipeline(pipelineInput); - - // Then the user shouldn't be able to delete them - UserUtils.switchUser(UserUtils.userWithAdapterAndPipelineAdminRights); - ConnectUtils.deleteAdapterAndAssociatedPipelinesPermissionDenied(); - }); -}); diff --git a/ui/cypress/tests/connect/enhancedAdapterDeletion.smoke.spec.ts b/ui/cypress/tests/connect/enhancedAdapterDeletion.smoke.spec.ts new file mode 100644 index 0000000000..e10fd81873 --- /dev/null +++ b/ui/cypress/tests/connect/enhancedAdapterDeletion.smoke.spec.ts @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; +import { UserUtils } from '../../support/utils/UserUtils'; +import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils'; + +describe('Test Enhanced Adapter Deletion', () => { + beforeEach('Setup Test', () => { + cy.initStreamPipesTest(); + UserUtils.addUser(UserUtils.userWithAdapterAndPipelineAdminRights); + }); + + it('Test Delete Adapter and Associated Pipelines', () => { + PipelineUtils.addSampleAdapterAndPipeline(); + + ConnectUtils.deleteAdapterAndAssociatedPipelines(); + }); + + it('Test Admin Should Be Able to Delete Adapter and Not Owned Associated Pipelines', () => { + // Let the user create the adapter and the pipeline + PipelineUtils.addSampleAdapterAndPipeline(); + + // Then let the admin delete them + UserUtils.switchUser(UserUtils.adminUser); + ConnectUtils.deleteAdapterAndAssociatedPipelines(true); + }); +}); diff --git a/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts b/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts index 21a1604fb3..71f2356ed9 100644 --- a/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts +++ b/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts @@ -123,7 +123,10 @@ function getLocalizedDateString(date: Date) { } function getLocalizedTimeString(date: Date) { - return date.toLocaleTimeString().slice(0, 8); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${hours}:${minutes}:${seconds}`; } function parseTimeStringToSeconds(timeString: string) { diff --git a/ui/cypress/tests/pipeline/multiUser/pipelineMultiUserSupport.ts b/ui/cypress/tests/pipeline/multiUser/pipelineMultiUserSupport.ts new file mode 100644 index 0000000000..31f61716e4 --- /dev/null +++ b/ui/cypress/tests/pipeline/multiUser/pipelineMultiUserSupport.ts @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { UserUtils } from '../../../support/utils/UserUtils'; +import { UserRole } from '../../../../src/app/_enums/user-role.enum'; +import { PipelineUtils } from '../../../support/utils/pipeline/PipelineUtils'; + +describe('Test Pipeline Multi User support', () => { + beforeEach('Setup Test', () => { + cy.initStreamPipesTest(); + }); + + it('Test with pipeline admin account', () => { + testPipelineNotShownForOtherUsers(UserRole.ROLE_PIPELINE_ADMIN); + }); + + it('Test with pipeline user account', () => { + testPipelineNotShownForOtherUsers(UserRole.ROLE_PIPELINE_USER); + }); + + /** + * This function validates that the pipeline is only shown to + * the user who created it + */ + function testPipelineNotShownForOtherUsers(userRole: UserRole) { + const pipelineAdminUser = UserUtils.createUser('user1', userRole); + + PipelineUtils.addSampleAdapterAndPipeline(); + + UserUtils.switchUser(pipelineAdminUser); + + PipelineUtils.checkAmountOfPipelinesPipeline(0); + } +}); diff --git a/ui/cypress/tests/pipeline/pipelineTest.smoke.spec.ts b/ui/cypress/tests/pipeline/pipelineTest.smoke.spec.ts index 010f7695f9..f17be7abc5 100644 --- a/ui/cypress/tests/pipeline/pipelineTest.smoke.spec.ts +++ b/ui/cypress/tests/pipeline/pipelineTest.smoke.spec.ts @@ -16,36 +16,15 @@ * */ -import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils'; -import { PipelineElementBuilder } from '../../support/builder/PipelineElementBuilder'; -import { PipelineBuilder } from '../../support/builder/PipelineBuilder'; - -const adapterName = 'simulator'; describe('Test Random Data Simulator Stream Adapter', () => { beforeEach('Setup Test', () => { cy.initStreamPipesTest(); - ConnectUtils.addMachineDataSimulator(adapterName); }); it('Perform Test', () => { - const pipelineInput = PipelineBuilder.create('Pipeline Test') - .addSource(adapterName) - .addProcessingElement( - PipelineElementBuilder.create('field_renamer') - .addInput('drop-down', 'convert-property', 'timestamp') - .addInput('input', 'field-name', 't') - .build(), - ) - .addSink( - PipelineElementBuilder.create('data_lake') - .addInput('input', 'db_measurement', 'demo') - .build(), - ) - .build(); - - PipelineUtils.addPipeline(pipelineInput); + PipelineUtils.addSampleAdapterAndPipeline(); PipelineUtils.deletePipeline(); }); }); diff --git a/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts b/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts index 5de2359906..f1ff02d0c9 100644 --- a/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts +++ b/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts @@ -16,36 +16,15 @@ * */ -import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils'; -import { PipelineElementBuilder } from '../../support/builder/PipelineElementBuilder'; -import { PipelineBuilder } from '../../support/builder/PipelineBuilder'; - -const adapterName = 'simulator'; describe('Test update of running pipeline', () => { beforeEach('Setup Test', () => { cy.initStreamPipesTest(); - ConnectUtils.addMachineDataSimulator(adapterName); }); it('Perform Test', () => { - const pipelineInput = PipelineBuilder.create('Pipeline Test') - .addSource(adapterName) - .addProcessingElement( - PipelineElementBuilder.create('field_renamer') - .addInput('drop-down', 'convert-property', 'timestamp') - .addInput('input', 'field-name', 't') - .build(), - ) - .addSink( - PipelineElementBuilder.create('data_lake') - .addInput('input', 'db_measurement', 'demo') - .build(), - ) - .build(); - - PipelineUtils.addPipeline(pipelineInput); + PipelineUtils.addSampleAdapterAndPipeline(); PipelineUtils.editPipeline(); cy.wait(1000); PipelineUtils.startPipeline(); diff --git a/ui/cypress/tests/userManagement/testGroupManagement.spec.ts b/ui/cypress/tests/userManagement/testGroupManagement.spec.ts index 03d7159568..4c62669b2b 100644 --- a/ui/cypress/tests/userManagement/testGroupManagement.spec.ts +++ b/ui/cypress/tests/userManagement/testGroupManagement.spec.ts @@ -23,6 +23,8 @@ import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils'; import { PipelineElementBuilder } from '../../support/builder/PipelineElementBuilder'; import { PipelineBuilder } from '../../support/builder/PipelineBuilder'; +import { GeneralUtils } from '../../support/utils/GeneralUtils'; +import { PermissionUtils } from '../../support/utils/user/PermissionUtils'; describe('Test Group Management for Pipelines', () => { beforeEach('Setup Test', () => { @@ -94,18 +96,15 @@ describe('Test Group Management for Pipelines', () => { // Add user group to pipeline PipelineUtils.goToPipelines(); - cy.dataCy('share').click(); + PermissionUtils.openManagePermissions(); cy.get('label').contains('Authorized Groups').click(); cy.get('mat-option').contains('User_Group').click(); - cy.dataCy('sp-element-edit-user-save').click(); + PermissionUtils.save(); // Login as first user which belongs to user group with pipeline admin role UserUtils.switchUser(user); - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 4, - ); + GeneralUtils.validateAmountOfNavigationIcons(4); // Check if pipeline is visible PipelineUtils.goToPipelines(); @@ -121,10 +120,7 @@ describe('Test Group Management for Pipelines', () => { // Login as user2 UserUtils.switchUser(user2); - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 3, - ); + GeneralUtils.validateAmountOfNavigationIcons(3); // Check if pipeline is invisible to user2 PipelineUtils.goToPipelines(); diff --git a/ui/cypress/tests/userManagement/testUserRoleConnect.spec.ts b/ui/cypress/tests/userManagement/testUserRoleConnect.spec.ts index 5b0dace368..04290d02ca 100644 --- a/ui/cypress/tests/userManagement/testUserRoleConnect.spec.ts +++ b/ui/cypress/tests/userManagement/testUserRoleConnect.spec.ts @@ -15,48 +15,53 @@ * limitations under the License. * */ -import { UserBuilder } from '../../support/builder/UserBuilder'; import { UserRole } from '../../../src/app/_enums/user-role.enum'; import { UserUtils } from '../../support/utils/UserUtils'; import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; -import { ConnectBtns } from '../../support/utils/connect/ConnectBtns'; +import { PermissionUtils } from '../../support/utils/user/PermissionUtils'; +import { GeneralUtils } from '../../support/utils/GeneralUtils'; describe('Test User Roles for Connect', () => { + var connectAdminUser; beforeEach('Setup Test', () => { cy.initStreamPipesTest(); + connectAdminUser = UserUtils.createUser( + 'user', + UserRole.ROLE_CONNECT_ADMIN, + ); ConnectUtils.addMachineDataSimulator('simulator'); }); - it('Perform Test', () => { - // Add connect admin user - const connect_admin = UserBuilder.create('user@streampipes.apache.org') - .setName('connect_admin') - .setPassword('password') - .addRole(UserRole.ROLE_CONNECT_ADMIN) - .build(); - UserUtils.addUser(connect_admin); - - // Login as user and check if connect is visible to user - UserUtils.switchUser(connect_admin); - - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 3, - ); + it('Connect admin should not see adapters of other users', () => { + UserUtils.switchUser(connectAdminUser); - ConnectUtils.goToConnect(); - cy.dataCy('all-adapters-table', { timeout: 10000 }).should( - 'have.length', - 1, - ); - cy.dataCy('all-adapters-table', { timeout: 10000 }).should( - 'contain', - 'simulator', - ); + GeneralUtils.validateAmountOfNavigationIcons(3); + + // Validate that no adapter is visible + ConnectUtils.checkAmountOfAdapters(0); + }); + + it('Connect admin should see public adapters of other users', () => { + // Set adapter to public + PermissionUtils.markElementAsPublic(); + + UserUtils.switchUser(connectAdminUser); + + GeneralUtils.validateAmountOfNavigationIcons(3); + + // Validate that adapter is visible + ConnectUtils.checkAmountOfAdapters(1); + }); + + it('Connect admin should see shared adapters of other users', () => { + // Share adapter with user + PermissionUtils.authorizeUser(connectAdminUser.email); + + UserUtils.switchUser(connectAdminUser); + + GeneralUtils.validateAmountOfNavigationIcons(3); - // validate that adapter can be stopped and edited - ConnectBtns.stopAdapter().click(); - ConnectBtns.editAdapter().should('not.be.disabled'); - ConnectBtns.editAdapter().click(); + // Validate that adapter is visible + ConnectUtils.checkAmountOfAdapters(1); }); }); diff --git a/ui/cypress/tests/userManagement/testUserRolePipeline.spec.ts b/ui/cypress/tests/userManagement/testUserRolePipeline.spec.ts index 94d83418ff..3c690ffe92 100644 --- a/ui/cypress/tests/userManagement/testUserRolePipeline.spec.ts +++ b/ui/cypress/tests/userManagement/testUserRolePipeline.spec.ts @@ -16,88 +16,90 @@ * */ -import { UserBuilder } from '../../support/builder/UserBuilder'; import { UserRole } from '../../../src/app/_enums/user-role.enum'; import { UserUtils } from '../../support/utils/UserUtils'; import { ConnectUtils } from '../../support/utils/connect/ConnectUtils'; import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils'; -import { PipelineElementBuilder } from '../../support/builder/PipelineElementBuilder'; -import { PipelineBuilder } from '../../support/builder/PipelineBuilder'; +import { GeneralUtils } from '../../support/utils/GeneralUtils'; +import { PermissionUtils } from '../../support/utils/user/PermissionUtils'; +import { PipelineBtns } from '../../support/utils/pipeline/PipelineBtns'; describe('Test User Roles for Pipelines', () => { beforeEach('Setup Test', () => { cy.initStreamPipesTest(); - ConnectUtils.addMachineDataSimulator('simulator'); - const pipelineInput = PipelineBuilder.create('Pipeline Test') - .addSource('simulator') - .addProcessingElement( - PipelineElementBuilder.create('field_renamer') - .addInput('drop-down', 'convert-property', 'timestamp') - .addInput('input', 'field-name', 't') - .build(), - ) - .addSink( - PipelineElementBuilder.create('data_lake') - .addInput('input', 'db_measurement', 'demo') - .build(), - ) - .build(); - - PipelineUtils.addPipeline(pipelineInput); + // Create a machine data simulator with a sample pipeline for the tests + ConnectUtils.addMachineDataSimulator('simulator', true); }); - it('Perform Test', () => { - // Add new user - UserUtils.goToUserConfiguration(); - - cy.dataCy('user-accounts-table-row', { timeout: 10000 }).should( - 'have.length', - 1, + it('Pipeline admin should not see pipelines of other users', () => { + const newUser = UserUtils.createUser( + 'user', + UserRole.ROLE_PIPELINE_ADMIN, ); - const email = 'user@streampipes.apache.org'; - const name = 'test_user'; - const user = UserBuilder.create(email) - .setName(name) - .setPassword(name) - .addRole(UserRole.ROLE_PIPELINE_USER) - .build(); + // Login as user and check if pipeline is visible to user + UserUtils.switchUser(newUser); + + GeneralUtils.validateAmountOfNavigationIcons(4); - UserUtils.addUser(user); + PipelineUtils.goToPipelines(); + PipelineUtils.checkAmountOfPipelinesPipeline(0); + }); - // Check if user is added successfully - cy.dataCy('user-accounts-table-row', { timeout: 10000 }).should( - 'have.length', - 2, + it('Pipeline admin should see public pipelines of other users', () => { + const newUser = UserUtils.createUser( + 'user', + UserRole.ROLE_PIPELINE_ADMIN, ); // Add new authorized user to pipeline PipelineUtils.goToPipelines(); - cy.dataCy('share').click(); - cy.get('label').contains('Authorized Users').click(); - cy.get('mat-option').contains(email).click(); - cy.dataCy('sp-element-edit-user-save').click(); + PermissionUtils.markElementAsPublic(); // Login as user and check if pipeline is visible to user - UserUtils.switchUser(user); + UserUtils.switchUser(newUser); + + PipelineUtils.goToPipelines(); + PipelineUtils.checkAmountOfPipelinesPipeline(1); + }); - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 3, + it(' Pipeline admin should see shared pipelines of other users', () => { + const newUser = UserUtils.createUser( + 'user', + UserRole.ROLE_PIPELINE_ADMIN, ); + // Add new authorized user to pipeline PipelineUtils.goToPipelines(); - cy.dataCy('all-pipelines-table', { timeout: 10000 }).should( - 'have.length', - 1, - ); - cy.dataCy('all-pipelines-table', { timeout: 10000 }).should( - 'contain', - 'Pipeline Test', + PermissionUtils.markElementAsPublic(); + PermissionUtils.authorizeUser(newUser.email); + + // Login as user and check if pipeline is visible to user + UserUtils.switchUser(newUser); + + PipelineUtils.goToPipelines(); + PipelineUtils.checkAmountOfPipelinesPipeline(1); + }); + + it(' Pipeline user should see shared pipelines of other users but not be able to edit them', () => { + const newUser = UserUtils.createUser( + 'user', + UserRole.ROLE_PIPELINE_USER, ); - // Delete user - UserUtils.switchUser(UserUtils.adminUser); - UserUtils.deleteUser(user); + // Add new authorized user to pipeline + PipelineUtils.goToPipelines(); + // PermissionUtils.markElementAsPublic(); + PermissionUtils.authorizeUser(newUser.email); + + // Login as user and check if pipeline is visible to user + UserUtils.switchUser(newUser); + + PipelineUtils.goToPipelines(); + PipelineUtils.checkAmountOfPipelinesPipeline(1); + + // A pipeline user should not be able to stop the pipeline or delete it + PipelineBtns.deletePipeline().should('not.exist'); + PipelineBtns.stopPipeline().should('be.disabled'); }); }); diff --git a/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts b/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts index 34e4624d63..4c00e01667 100644 --- a/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts +++ b/ui/cypress/tests/userManagement/testVariousUserRoles.smoke.spec.ts @@ -19,6 +19,7 @@ import { UserBuilder } from '../../support/builder/UserBuilder'; import { UserRole } from '../../../src/app/_enums/user-role.enum'; import { UserUtils } from '../../support/utils/UserUtils'; +import { GeneralUtils } from '../../support/utils/GeneralUtils'; const testedRoles = [ UserRole.ROLE_PIPELINE_ADMIN, @@ -38,10 +39,7 @@ for (var i = 0; i < testedRoles.length; i++) { it('Perform Test', () => { // Add new user UserUtils.goToUserConfiguration(); - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 8, - ); + GeneralUtils.validateAmountOfNavigationIcons(8); cy.dataCy('user-accounts-table-row', { timeout: 10000 }).should( 'have.length', @@ -68,30 +66,15 @@ for (var i = 0; i < testedRoles.length; i++) { // Check if every role displays correct navigation menu if (testRole == UserRole.ROLE_PIPELINE_ADMIN) { - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 4, - ); + GeneralUtils.validateAmountOfNavigationIcons(4); } else if (testRole == UserRole.ROLE_DASHBOARD_ADMIN) { - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 4, - ); + GeneralUtils.validateAmountOfNavigationIcons(4); } else if (testRole == UserRole.ROLE_DATA_EXPLORER_ADMIN) { - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 4, - ); + GeneralUtils.validateAmountOfNavigationIcons(4); } else if (testRole == UserRole.ROLE_CONNECT_ADMIN) { - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 3, - ); + GeneralUtils.validateAmountOfNavigationIcons(3); } else if (testRole == UserRole.ROLE_ASSET_ADMIN) { - cy.dataCy('navigation-icon', { timeout: 10000 }).should( - 'have.length', - 3, - ); + GeneralUtils.validateAmountOfNavigationIcons(3); } // Login as admin and delete user diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html index 5e8302acb3..52024357ff 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html +++ b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html @@ -24,7 +24,11 @@ - + No entries available. diff --git a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html index 5c1b11994c..6147d2cf9c 100644 --- a/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html +++ b/ui/src/app/connect/components/existing-adapters/existing-adapters.component.html @@ -258,6 +258,7 @@
mat-icon-button matTooltip="Manage permissions" matTooltipPosition="above" + data-cy="open-manage-permissions" *ngIf="isAdmin" (click)="showPermissionsDialog(adapter)" > diff --git a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html index cf16da3273..ba806e1b7b 100644 --- a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html +++ b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html @@ -33,7 +33,10 @@

{{ headerTitle }}

> - + Public Element @@ -66,6 +69,7 @@

{{ headerTitle }}

[matChipInputSeparatorKeyCodes]=" separatorKeysCodes " + data-cy="authorized-user" (matChipInputTokenEnd)="addUser($event)" /> @@ -76,6 +80,7 @@

{{ headerTitle }}

{{ user.username }} @@ -140,7 +145,7 @@

{{ headerTitle }}

(click)="save()" style="margin-right: 10px" [disabled]="!parentForm.valid" - data-cy="sp-element-edit-user-save" + data-cy="sp-manage-permissions-save" > save Save diff --git a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts index 1f21a19d03..dc9e66c336 100644 --- a/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts +++ b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts @@ -231,7 +231,7 @@ export class ObjectPermissionDialogComponent implements OnInit { private addUserToSelection(authority: PermissionEntry) { const user = this.allUsers.find(u => u.principalId === authority.sid); - this.grantedUserAuthorities.push(user); + user && this.grantedUserAuthorities.push(user); } private addGroupToSelection(authority: PermissionEntry) { diff --git a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html index f9918daaa0..1c1bf59463 100644 --- a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html +++ b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html @@ -28,6 +28,7 @@
share