This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch STREAMPIPES-426 in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git
commit c4e23c37265abcfd7321792ea17d51d79724fefa Author: Dominik Riemer <[email protected]> AuthorDate: Wed Oct 20 17:00:09 2021 +0200 [STREAMPIPES-426] Improve permission management --- pom.xml | 6 + .../backend/StreamPipesResourceConfig.java | 2 + .../streampipes/model/client/user/Permission.java | 27 +++- .../model/client/user/PermissionBuilder.java | 4 +- .../model/client/user/PermissionEntry.java | 63 ++++++++ .../model/client/user/PrincipalType.java | 3 +- .../manager/pipeline/PipelineManager.java | 3 +- .../manager/setup/CouchDbInstallationStep.java | 6 +- .../manager/pipeline/TestPipelineManager.java | 99 +++++-------- .../management/PermissionResourceManager.java | 26 ++-- .../resource/management/SpResourceManager.java | 4 + .../rest/core/base/impl/AbstractRestResource.java | 5 + streampipes-rest/pom.xml | 6 + .../streampipes/rest/impl/PipelineResource.java | 6 - .../apache/streampipes/rest/impl/UserResource.java | 6 +- .../rest/impl/admin/PermissionResource.java | 14 +- .../storage/api/IPermissionStorage.java | 2 + .../couchdb/impl/PermissionStorageImpl.java | 10 ++ .../StreamPipesCredentialsMatcher.java | 20 +-- .../authentication/StreamPipesRealm.java | 109 -------------- ui/angular.json | 2 + ui/src/app/app-overview/app-overview.component.ts | 2 +- .../app/core-model/gen/streampipes-model-client.ts | 64 +++++--- ui/src/app/core-ui/core-ui.module.ts | 2 + .../object-permission-dialog.component.html | 83 +++++++++++ .../object-permission-dialog.component.scss | 10 +- .../object-permission-dialog.component.ts | 165 +++++++++++++++++++++ .../pipeline-overview.component.html | 44 ++++-- .../pipeline-overview.component.ts | 10 +- .../services/pipeline-operations.service.ts | 98 +++++++----- .../platform-services/apis/permissions.service.ts | 48 ++++++ ui/src/app/platform-services/apis/user.service.ts | 17 ++- ui/src/app/platform-services/platform.module.ts | 5 +- ui/src/app/services/auth.service.ts | 3 +- 34 files changed, 665 insertions(+), 309 deletions(-) diff --git a/pom.xml b/pom.xml index 625b419..a045909 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ <influxdb.version>2.21</influxdb.version> <jackson.version>2.12.3</jackson.version> <jakarta-annotation.version>1.3.5</jakarta-annotation.version> + <jakarta-mail.version>1.6.7</jakarta-mail.version> <jakarta-validation.version>2.0.2</jakarta-validation.version> <javapoet.version>1.6.1</javapoet.version> <javax-annotation.version>1.3.2</javax-annotation.version> @@ -229,6 +230,11 @@ <version>${retrofit.version}</version> </dependency> <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + <version>${jakarta-mail.version}</version> + </dependency> + <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>${commons-beanutils.version}</version> diff --git a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java index 7ae8e19..de5ab6f 100644 --- a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java +++ b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java @@ -25,6 +25,7 @@ import org.apache.streampipes.ps.PipelineElementTemplateResource; import org.apache.streampipes.rest.impl.*; import org.apache.streampipes.rest.impl.admin.ConsulConfig; import org.apache.streampipes.rest.impl.admin.EmailConfigurationResource; +import org.apache.streampipes.rest.impl.admin.PermissionResource; import org.apache.streampipes.rest.impl.admin.UserGroupResource; import org.apache.streampipes.rest.impl.connect.*; import org.apache.streampipes.rest.impl.dashboard.Dashboard; @@ -73,6 +74,7 @@ public class StreamPipesResourceConfig extends ResourceConfig { register(MeasurementUnitResource.class); register(Notification.class); register(OntologyMeasurementUnit.class); + register(PermissionResource.class); register(PersistedDataStreamResource.class); register(PipelineCanvasMetadataCache.class); register(PipelineCanvasMetadataResource.class); diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java index 2c0f2cb..d1ed397 100644 --- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java +++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Permission.java @@ -19,10 +19,12 @@ package org.apache.streampipes.model.client.user; import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.gson.annotations.SerializedName; +import org.apache.streampipes.model.shared.annotation.TsModel; import java.util.ArrayList; import java.util.List; +@TsModel public class Permission { protected @SerializedName("_id") String permissionId; @@ -33,13 +35,14 @@ public class Permission { private String objectInstanceId; private String objectClassName; + private boolean publicElement; private String ownerSid; - private List<String> allowedSids; + private List<PermissionEntry> grantedAuthorities; public Permission() { - this.allowedSids = new ArrayList<>(); + this.grantedAuthorities = new ArrayList<>(); } public String getPermissionId() { @@ -82,16 +85,16 @@ public class Permission { this.ownerSid = ownerSid; } - public void addAllowedSid(String sid) { - this.allowedSids.add(sid); + public void addPermissionEntry(PermissionEntry permissionEntry) { + this.grantedAuthorities.add(permissionEntry); } - public List<String> getAllowedSids() { - return allowedSids; + public List<PermissionEntry> getGrantedAuthorities() { + return grantedAuthorities; } - public void setAllowedSids(List<String> allowedSids) { - this.allowedSids = allowedSids; + public void setGrantedAuthorities(List<PermissionEntry> grantedAuthorities) { + this.grantedAuthorities = grantedAuthorities; } public String get$type() { @@ -101,4 +104,12 @@ public class Permission { public void set$type(String $type) { this.$type = $type; } + + public boolean isPublicElement() { + return publicElement; + } + + public void setPublicElement(boolean publicElement) { + this.publicElement = publicElement; + } } diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java index 70da030..f739f29 100644 --- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java +++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java @@ -40,8 +40,8 @@ public class PermissionBuilder { this.permission.setOwnerSid(ownerSid); } - public PermissionBuilder with(String sid) { - this.permission.addAllowedSid(sid); + public PermissionBuilder with(PermissionEntry permissionEntry) { + this.permission.addPermissionEntry(permissionEntry); return this; } diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionEntry.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionEntry.java new file mode 100644 index 0000000..f9dd25d --- /dev/null +++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionEntry.java @@ -0,0 +1,63 @@ +/* + * 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. + * + */ +package org.apache.streampipes.model.client.user; + +import java.util.Objects; + +public class PermissionEntry { + + private String sid; + private PrincipalType principalType; + + public PermissionEntry() { + } + + public PermissionEntry(String sid, PrincipalType principalType) { + this.sid = sid; + this.principalType = principalType; + } + + public String getSid() { + return sid; + } + + public void setSid(String sid) { + this.sid = sid; + } + + public PrincipalType getPrincipalType() { + return principalType; + } + + public void setPrincipalType(PrincipalType principalType) { + this.principalType = principalType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PermissionEntry that = (PermissionEntry) o; + return sid.equals(that.sid) && principalType == that.principalType; + } + + @Override + public int hashCode() { + return Objects.hash(sid, principalType); + } +} diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PrincipalType.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PrincipalType.java index be84b20..620c9a4 100644 --- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PrincipalType.java +++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PrincipalType.java @@ -20,5 +20,6 @@ package org.apache.streampipes.model.client.user; public enum PrincipalType { USER_ACCOUNT, - SERVICE_ACCOUNT + SERVICE_ACCOUNT, + GROUP } diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/PipelineManager.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/PipelineManager.java index c1a0680..6a077a0 100644 --- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/PipelineManager.java +++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/PipelineManager.java @@ -66,7 +66,8 @@ public class PipelineManager { * @param pipeline * @return the pipelineId */ - public static String addPipeline(String principalSid, Pipeline pipeline) { + public static String addPipeline(String principalSid, + Pipeline pipeline) { // call by reference bad smell String pipelineId = UUIDGenerator.generateUuid(); diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java index c1a05f8..76f0160 100644 --- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java +++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java @@ -157,7 +157,10 @@ public class CouchDbInstallationStep extends InstallationStep { tokenFunction.setMap("function(doc) { if (doc.properties.userApiTokens) { doc.properties.userApiTokens.forEach(function(token) { emit(token.properties.hashedToken, doc.properties.email); });}}"); MapReduce userPermissionFunction = new MapReduce(); - userPermissionFunction.setMap("function(doc) { if (doc.$type === 'permission') {emit(doc.ownerSid, doc); for(var i = 0; i < doc.allowedSids.length; i++) {emit(doc.allowedSids[i],doc)}}}"); + userPermissionFunction.setMap("function(doc) { if (doc.$type === 'permission') {emit(doc.ownerSid, doc); for(var i = 0; i < doc.grantedAuthorities.length; i++) {emit(doc.grantedAuthorities[i].sid,doc)}}}"); + + MapReduce objectPermissionFunction = new MapReduce(); + objectPermissionFunction.setMap("function(doc) { if (doc.$type === 'permission') {emit(doc.objectInstanceId, doc);}}"); views.put("password", passwordFunction); views.put("username", usernameFunction); @@ -165,6 +168,7 @@ public class CouchDbInstallationStep extends InstallationStep { views.put("permissions", permissionFunction); views.put("token", tokenFunction); views.put("userpermissions", userPermissionFunction); + views.put("objectpermissions", objectPermissionFunction); userDocument.setViews(views); Response resp = Utils.getCouchDbUserClient().design().synchronizeWithDb(userDocument); diff --git a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/TestPipelineManager.java b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/TestPipelineManager.java index b5a33e0..88bc61b 100644 --- a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/TestPipelineManager.java +++ b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/TestPipelineManager.java @@ -41,62 +41,43 @@ import static junit.framework.TestCase.assertNotNull; Operations.class}) public class TestPipelineManager { - @Before - public void before() { - PowerMockito.mockStatic( - UserManagementService.class); - } - - @Test - public void testStartPipeline() { - // Prepare - PipelineOperationStatus expectedPipelineOperationStatus = getPipelineOperationStatus(); - PowerMockito.stub(PowerMockito.method(PipelineManager.class, "getPipeline", String.class)).toReturn(DummyPipelineGenerator.makePipelineWithPipelineName()); - PowerMockito.stub(PowerMockito.method(Operations.class, "startPipeline", Pipeline.class)).toReturn(expectedPipelineOperationStatus); - - // Test - PipelineOperationStatus result = PipelineManager.startPipeline("pipelineId"); - - // Assertions - assertNotNull(result); - assertEquals(expectedPipelineOperationStatus.getPipelineName(), result.getPipelineName()); - } - - @Test - public void testStopPipeline() { - // Prepare - PipelineOperationStatus expectedPipelineOperationStatus = getPipelineOperationStatus(); - PowerMockito.stub(PowerMockito.method(PipelineManager.class, "getPipeline", String.class)).toReturn(DummyPipelineGenerator.makePipelineWithPipelineName()); - PowerMockito.stub(PowerMockito.method(Operations.class, "stopPipeline", Pipeline.class, boolean.class)).toReturn(expectedPipelineOperationStatus); - - // Test - PipelineOperationStatus result = PipelineManager.stopPipeline("pipelineId", true); - - // Assertions - assertNotNull(result); - assertEquals(expectedPipelineOperationStatus.getPipelineName(), result.getPipelineName()); - } - - @Test - public void testAddPipeline() { - // Prepare - PowerMockito.mockStatic(Operations.class); - Pipeline pipeline = DummyPipelineGenerator.makePipelineWithProcessorAndSink(); - - String username = "[email protected]"; - - // Test - String result = PipelineManager.addPipeline(username, pipeline); - - // Assertions - assertNotNull(result); - assertNotNull(pipeline.getPipelineId()); - assertEquals(username, pipeline.getCreatedByUser()); - pipeline.getSepas().forEach(processor -> assertEquals(username, processor.getCorrespondingUser())); - pipeline.getActions().forEach(sink -> assertEquals(username, sink.getCorrespondingUser())); - } - - private PipelineOperationStatus getPipelineOperationStatus() { - return new PipelineOperationStatus("", DummyPipelineGenerator.PIPELINE_NAME,"", new ArrayList<>()); - } -} \ No newline at end of file + @Before + public void before() { + PowerMockito.mockStatic( + UserManagementService.class); + } + + @Test + public void testStartPipeline() { + // Prepare + PipelineOperationStatus expectedPipelineOperationStatus = getPipelineOperationStatus(); + PowerMockito.stub(PowerMockito.method(PipelineManager.class, "getPipeline", String.class)).toReturn(DummyPipelineGenerator.makePipelineWithPipelineName()); + PowerMockito.stub(PowerMockito.method(Operations.class, "startPipeline", Pipeline.class)).toReturn(expectedPipelineOperationStatus); + + // Test + PipelineOperationStatus result = PipelineManager.startPipeline("pipelineId"); + + // Assertions + assertNotNull(result); + assertEquals(expectedPipelineOperationStatus.getPipelineName(), result.getPipelineName()); + } + + @Test + public void testStopPipeline() { + // Prepare + PipelineOperationStatus expectedPipelineOperationStatus = getPipelineOperationStatus(); + PowerMockito.stub(PowerMockito.method(PipelineManager.class, "getPipeline", String.class)).toReturn(DummyPipelineGenerator.makePipelineWithPipelineName()); + PowerMockito.stub(PowerMockito.method(Operations.class, "stopPipeline", Pipeline.class, boolean.class)).toReturn(expectedPipelineOperationStatus); + + // Test + PipelineOperationStatus result = PipelineManager.stopPipeline("pipelineId", true); + + // Assertions + assertNotNull(result); + assertEquals(expectedPipelineOperationStatus.getPipelineName(), result.getPipelineName()); + } + + private PipelineOperationStatus getPipelineOperationStatus() { + return new PipelineOperationStatus("", DummyPipelineGenerator.PIPELINE_NAME, "", new ArrayList<>()); + } +} diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPermissionStorage.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java similarity index 60% copy from streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPermissionStorage.java copy to streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java index 65ac07c..f19a634 100644 --- a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPermissionStorage.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java @@ -15,25 +15,27 @@ * limitations under the License. * */ -package org.apache.streampipes.storage.api; +package org.apache.streampipes.resource.management; import org.apache.streampipes.model.client.user.Permission; +import org.apache.streampipes.storage.api.IPermissionStorage; +import org.apache.streampipes.storage.management.StorageDispatcher; import java.util.List; -import java.util.Set; -public interface IPermissionStorage { +public class PermissionResourceManager { - List<Permission> getAllPermissions(); + private final IPermissionStorage db; - Permission getPermissionById(String permissionId); + public PermissionResourceManager() { + this.db = StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage(); + } - void addPermission(Permission permission); - - void updatePermission(Permission permission); - - void deletePermission(String permissionId); - - Set<String> getObjectPermissions(List<String> sids); + public List<Permission> findAll() { + return db.getAllPermissions(); + } + public List<Permission> findForObjectId(String objectInstanceId) { + return db.getUserPermissionsForObject(objectInstanceId); + } } diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java index 88e56f2..e5304b0 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java @@ -38,4 +38,8 @@ public class SpResourceManager { public PipelineResourceManager managePipelines() { return new PipelineResourceManager(); } + + public PermissionResourceManager managePermissions() { + return new PermissionResourceManager(); + } } diff --git a/streampipes-rest-core-base/src/main/java/org/apache/streampipes/rest/core/base/impl/AbstractRestResource.java b/streampipes-rest-core-base/src/main/java/org/apache/streampipes/rest/core/base/impl/AbstractRestResource.java index 92796fc..43fd507 100644 --- a/streampipes-rest-core-base/src/main/java/org/apache/streampipes/rest/core/base/impl/AbstractRestResource.java +++ b/streampipes-rest-core-base/src/main/java/org/apache/streampipes/rest/core/base/impl/AbstractRestResource.java @@ -26,6 +26,7 @@ import org.apache.streampipes.model.message.ErrorMessage; import org.apache.streampipes.model.message.Message; import org.apache.streampipes.model.message.Notification; import org.apache.streampipes.model.message.SuccessMessage; +import org.apache.streampipes.resource.management.SpResourceManager; import org.apache.streampipes.rest.shared.impl.AbstractSharedRestInterface; import org.apache.streampipes.storage.api.*; import org.apache.streampipes.storage.management.StorageDispatcher; @@ -148,4 +149,8 @@ public abstract class AbstractRestResource extends AbstractSharedRestInterface { return SpServiceDiscovery.getServiceDiscovery(); } + protected SpResourceManager getSpResourceManager() { + return new SpResourceManager(); + } + } diff --git a/streampipes-rest/pom.xml b/streampipes-rest/pom.xml index 332189d..3f73aa3 100644 --- a/streampipes-rest/pom.xml +++ b/streampipes-rest/pom.xml @@ -120,6 +120,12 @@ <artifactId>influxdb-java</artifactId> </dependency> + <!-- dependency convergence --> + <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>jakarta.mail</artifactId> + </dependency> + <!-- Test dependencies --> <dependency> <groupId>junit</groupId> 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 2f252b2..b155dfc 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 @@ -40,11 +40,8 @@ import org.apache.streampipes.model.pipeline.PipelineOperationStatus; import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource; import org.apache.streampipes.rest.security.AuthConstants; import org.apache.streampipes.rest.shared.annotation.JacksonSerialized; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.access.prepost.PreFilter; import org.springframework.stereotype.Component; import javax.ws.rs.*; @@ -56,8 +53,6 @@ import java.util.List; @Path("/v2/pipelines") public class PipelineResource extends AbstractAuthGuardedRestResource { - private static final Logger logger = LoggerFactory.getLogger(PipelineResource.class); - @GET @Produces(MediaType.APPLICATION_JSON) @Path("/own") @@ -146,7 +141,6 @@ public class PipelineResource extends AbstractAuthGuardedRestResource { @Operation(summary = "Stop the pipeline with the given id", tags = {"Pipeline"}) @PreAuthorize(AuthConstants.HAS_WRITE_PIPELINE_PRIVILEGE) - @PreFilter("hasPermission(pipelineId, 'WRITE')") public Response stop(@PathParam("pipelineId") String pipelineId, @QueryParam("forceStop") @DefaultValue("false") boolean forceStop) { try { diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java index c34b0ae..ce108f4 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java @@ -56,10 +56,10 @@ public class UserResource extends AbstractAuthGuardedRestResource { @GET @JacksonSerialized - @Path("{username}") + @Path("{principalId}") @Produces(MediaType.APPLICATION_JSON) - public Response getUserDetails(@PathParam("username") String username) { - Principal principal = getPrincipal(username); + public Response getUserDetails(@PathParam("principalId") String principalId) { + Principal principal = getPrincipalById(principalId); removeCredentials(principal); if (principal != null) { diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/PermissionResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/PermissionResource.java index 3c8cfd7..310b847 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/PermissionResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/PermissionResource.java @@ -17,10 +17,22 @@ */ package org.apache.streampipes.rest.impl.admin; +import org.apache.streampipes.model.client.user.Permission; +import org.apache.streampipes.rest.core.base.impl.AbstractRestResource; + +import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import java.util.List; @Path("/v2/admin/permissions") -public class PermissionResource { +public class PermissionResource extends AbstractRestResource { + + @GET + @Path("{objectInstanceId}") + public List<Permission> getPermissionForObject(@PathParam("objectInstanceId") String objectInstanceId) { + return getSpResourceManager().managePermissions().findForObjectId(objectInstanceId); + } } diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPermissionStorage.java b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPermissionStorage.java index 65ac07c..fadcc46 100644 --- a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPermissionStorage.java +++ b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPermissionStorage.java @@ -36,4 +36,6 @@ public interface IPermissionStorage { Set<String> getObjectPermissions(List<String> sids); + List<Permission> getUserPermissionsForObject(String objectInstanceId); + } diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/PermissionStorageImpl.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/PermissionStorageImpl.java index b518c4a..fef5df1 100644 --- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/PermissionStorageImpl.java +++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/PermissionStorageImpl.java @@ -70,6 +70,16 @@ public class PermissionStorageImpl extends CrudDao implements IPermissionStorage return toPermissionSet(objectInstanceSids); } + public List<Permission> getUserPermissionsForObject(String objectInstanceId) { + + return couchDbClientSupplier + .get() + .view("users/objectpermissions") + .key(objectInstanceId) + .includeDocs(true) + .query(Permission.class); + } + private Set<String> toPermissionSet(List<Permission> permissions) { return permissions.stream().map(Permission::getObjectInstanceId).collect(Collectors.toSet()); } diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesCredentialsMatcher.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesCredentialsMatcher.java index e4cbea6..cc0c94f 100644 --- a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesCredentialsMatcher.java +++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesCredentialsMatcher.java @@ -26,25 +26,11 @@ import java.security.spec.InvalidKeySpecException; public class StreamPipesCredentialsMatcher implements PasswordEncoder { -// @Override -// public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { -// try { -// return PasswordUtil.validatePassword(new String((char[]) authenticationToken -// .getCredentials()), -// authenticationInfo -// .getCredentials().toString()); -// } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { -// return false; -// } -// } - @Override public String encode(CharSequence charSequence) { try { return PasswordUtil.encryptPassword(charSequence.toString()); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (InvalidKeySpecException e) { + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { e.printStackTrace(); } return null; @@ -54,9 +40,7 @@ public class StreamPipesCredentialsMatcher implements PasswordEncoder { public boolean matches(CharSequence charSequence, String s) { try { return PasswordUtil.validatePassword(charSequence.toString(), s); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (InvalidKeySpecException e) { + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { e.printStackTrace(); } return false; diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java deleted file mode 100644 index c13552d..0000000 --- a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java +++ /dev/null @@ -1,109 +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. -// * -// */ -// -//package org.apache.streampipes.user.management.authentication; -// -// -//import org.apache.shiro.authc.*; -//import org.apache.shiro.realm.Realm; -//import org.apache.shiro.subject.SimplePrincipalCollection; -//import org.apache.streampipes.model.client.user.User; -//import org.apache.streampipes.user.management.service.TokenService; -//import org.apache.streampipes.user.management.service.UserService; -//import org.apache.streampipes.user.management.util.TokenUtil; -//import org.lightcouch.CouchDbException; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -// -// -///** -// * Principals are a subjects identifying attributes such as first name, last name or user name. -// * Credentials are usually secret values known only by the subject -// */ -//public class StreamPipesRealm implements Realm { -// -// private static final Logger LOG = LoggerFactory.getLogger(StreamPipesRealm.class); -// -// private StreamPipesCredentialsMatcher credentialsMatcher; -// -// public StreamPipesRealm() { -// this.credentialsMatcher = new StreamPipesCredentialsMatcher(); -// } -// -// @Override -// public String getName() { -// return "StreamPipeRealm"; -// } -// -// @Override -// /** -// * Check if type of AuthenticationToken is supported. -// * So far we only support UsernamePasswordToken. -// */ -// public boolean supports(AuthenticationToken authenticationToken) { -// return authenticationToken instanceof UsernamePasswordToken || authenticationToken instanceof BearerToken; -// } -// -// @Override -// /** -// * Checks if token is correct. See class JavaDoc for creating the password view. -// */ -// public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { -// -// if (authenticationToken instanceof UsernamePasswordToken) { -// -// try { -// String email = ((UsernamePasswordToken) authenticationToken).getUsername(); -// UserService userService = new UserService(email); -// -// SimpleAuthenticationInfo info = makeInfo(email); -// info.setCredentials(userService.getPassword()); -// -// if (credentialsMatcher.doCredentialsMatch(authenticationToken, info)) { -// LOG.info("User successfully authenticated"); -// } else { -// throw new AuthenticationException("Could not authenticate"); -// } -// return info; -// -// } catch (CouchDbException | NullPointerException e) { -// e.printStackTrace(); -// } -// } else if (authenticationToken instanceof BearerToken) { -// BearerToken token = (BearerToken) authenticationToken; -// String hashedToken = TokenUtil.hashToken(token.getToken()); -// User user = new TokenService().findUserForToken(hashedToken); -// SimpleAuthenticationInfo info = makeInfo(user.getEmail()); -// -// return info; -// } -// -// return null; -// } -// -// private SimpleAuthenticationInfo makeInfo(String email) { -// SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(); -// SimplePrincipalCollection principals = new SimplePrincipalCollection(); -// principals.add(email, this.getName()); -// -// LOG.info(principals.toString()); -// info.setPrincipals(principals); -// -// return info; -// } -//} diff --git a/ui/angular.json b/ui/angular.json index 7b233ae..bb40fdd 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -69,6 +69,8 @@ } }, "development": { + "sourceMap": true, + "optimization": false, "customWebpackConfig": { "path": "./webpack.partial.dev.js" } diff --git a/ui/src/app/app-overview/app-overview.component.ts b/ui/src/app/app-overview/app-overview.component.ts index 733bcfb..30ce1a2 100644 --- a/ui/src/app/app-overview/app-overview.component.ts +++ b/ui/src/app/app-overview/app-overview.component.ts @@ -16,7 +16,7 @@ * */ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit } from '@angular/core'; @Component({ templateUrl: './app-overview.component.html', diff --git a/ui/src/app/core-model/gen/streampipes-model-client.ts b/ui/src/app/core-model/gen/streampipes-model-client.ts index 93841c1..9f197fb 100644 --- a/ui/src/app/core-model/gen/streampipes-model-client.ts +++ b/ui/src/app/core-model/gen/streampipes-model-client.ts @@ -1,25 +1,7 @@ -/* - * 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. - * - */ - /* tslint:disable */ /* eslint-disable */ // @ts-nocheck -// Generated using typescript-generator version 2.27.744 on 2021-10-12 09:29:49. +// Generated using typescript-generator version 2.27.744 on 2021-10-19 16:24:30. export class Element { elementId: string; @@ -140,6 +122,46 @@ export class MatchingResultMessage { } } +export class Permission { + grantedAuthorities: PermissionEntry[]; + objectClassName: string; + objectInstanceId: string; + ownerSid: string; + permissionId: string; + publicElement: boolean; + rev: string; + + static fromData(data: Permission, target?: Permission): Permission { + if (!data) { + return data; + } + const instance = target || new Permission(); + instance.permissionId = data.permissionId; + instance.rev = data.rev; + instance.objectInstanceId = data.objectInstanceId; + instance.objectClassName = data.objectClassName; + instance.publicElement = data.publicElement; + instance.ownerSid = data.ownerSid; + instance.grantedAuthorities = __getCopyArrayFn(PermissionEntry.fromData)(data.grantedAuthorities); + return instance; + } +} + +export class PermissionEntry { + principalType: PrincipalType; + sid: string; + + static fromData(data: PermissionEntry, target?: PermissionEntry): PermissionEntry { + if (!data) { + return data; + } + const instance = target || new PermissionEntry(); + instance.sid = data.sid; + instance.principalType = data.principalType; + return instance; + } +} + export class Principal { accountEnabled: boolean; accountExpired: boolean; @@ -278,9 +300,9 @@ export class UserInfo { } } -export type PrincipalType = "USER_ACCOUNT" | "SERVICE_ACCOUNT"; +export type PrincipalType = "USER_ACCOUNT" | "SERVICE_ACCOUNT" | "GROUP"; -export type Privilege = "PRIVILEGE_CREATE_PIPELINE" | "PRIVILEGE_READ_PIPELINE" | "PRIVILEGE_UPDATE_PIPELINE" | "PRIVILEGE_DELETE_PIPELINE" | "PRIVILEGE_CREATE_ADAPTER" | "PRIVILEGE_READ_ADAPTER" | "PRIVILEGE_UPDATE_ADAPTER" | "PRIVILEGE_DELETE_ADAPTER" | "PRIVILEGE_CREATE_PIPELINE_ELEMENT" | "PRIVILEGE_READ_PIPELINE_ELEMENT" | "PRIVILEGE_UPDATE_PIPELINE_ELEMENT" | "PRIVILEGE_DELETE_PIPELINE_ELEMENT" | "PRIVILEGE_CREATE_DASHBOARD" | "PRIVILEGE_READ_DASHBOARD" | "PRIVILEGE_UPDATE_DASHBOAR [...] +export type Privilege = "PRIVILEGE_READ_PIPELINE" | "PRIVILEGE_WRITE_PIPELINE" | "PRIVILEGE_DELETE_PIPELINE" | "PRIVILEGE_READ_ADAPTER" | "PRIVILEGE_WRITE_ADAPTER" | "PRIVILEGE_DELETE_ADAPTER" | "PRIVILEGE_READ_PIPELINE_ELEMENT" | "PRIVILEGE_WRITE_PIPELINE_ELEMENT" | "PRIVILEGE_DELETE_PIPELINE_ELEMENT" | "PRIVILEGE_READ_DASHBOARD" | "PRIVILEGE_WRITE_DASHBOARD" | "PRIVILEGE_DELETE_DASHBOARD" | "PRIVILEGE_READ_DASHBOARD_WIDGET" | "PRIVILEGE_WRITE_DASHBOARD_WIDGET" | "PRIVILEGE_DELETE_DASHB [...] export type Role = "ROLE_ADMIN" | "ROLE_PIPELINE_ADMIN" | "ROLE_DASHBOARD_ADMIN" | "ROLE_DATA_EXPLORER_ADMIN" | "ROLE_CONNECT_ADMIN" | "ROLE_DASHBOARD_USER" | "ROLE_DATA_EXPLORER_USER" | "ROLE_PIPELINE_USER" | "ROLE_APP_USER"; diff --git a/ui/src/app/core-ui/core-ui.module.ts b/ui/src/app/core-ui/core-ui.module.ts index 9c9a578..82fe6c7 100644 --- a/ui/src/app/core-ui/core-ui.module.ts +++ b/ui/src/app/core-ui/core-ui.module.ts @@ -80,6 +80,7 @@ import { ErrorHintComponent } from './error-hint/error-hint.component'; import { AddToCollectionComponent } from './static-properties/static-collection/add-to-collection/add-to-collection.component'; import { PipelineStartedStatusComponent } from './pipeline/pipeline-started-status/pipeline-started-status.component'; import { SplitSectionComponent } from './split-section/split-section.component'; +import { ObjectPermissionDialogComponent } from './object-permission-dialog/object-permission-dialog.component'; @NgModule({ imports: [ @@ -116,6 +117,7 @@ import { SplitSectionComponent } from './split-section/split-section.component'; ImageBarComponent, ImageAnnotationsComponent, ImageViewerComponent, + ObjectPermissionDialogComponent, StandardDialogComponent, PanelDialogComponent, SplitSectionComponent, 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 new file mode 100644 index 0000000..d81393d --- /dev/null +++ b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.html @@ -0,0 +1,83 @@ +<!-- + ~ 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. + ~ + --> + +<div class="sp-dialog-container"> + <div class="sp-dialog-content" *ngIf="usersLoaded"> + <div fxFlex="100" fxLayout="column" class="p-15"> + <h4>{{headerTitle}}</h4> + <form [formGroup]="parentForm" fxFlex="100" fxLayout="column"> + <div class="general-options-panel" fxLayout="column"> + <span class="general-options-header">Basics</span> + <mat-form-field color="accent"> + <mat-label>Owner</mat-label> + <input formControlName="owner" fxFlex + matInput + required> + </mat-form-field> + <mat-checkbox formControlName="publicElement"> + Public Element + </mat-checkbox> + </div> + <div fxLayout="column" class="general-options-panel"> + <span class="general-options-header">Users</span> + <mat-form-field class="example-chip-list" appearance="fill"> + <mat-label>Authorized Users</mat-label> + <mat-chip-list #chipList aria-label="User selection"> + <mat-chip + *ngFor="let user of grantedUserAuthorities" + selectable="true" + removable="true" + (removed)="removeUser(user)"> + {{user.username}} + <button matChipRemove> + <mat-icon>cancel</mat-icon> + </button> + </mat-chip> + <input + placeholder="User Selection" + #userInput + [formControl]="userCtrl" + [matAutocomplete]="auto" + [matChipInputFor]="chipList" + [matChipInputSeparatorKeyCodes]="separatorKeysCodes" + (matChipInputTokenEnd)="addUser($event)"> + </mat-chip-list> + <mat-autocomplete #auto="matAutocomplete" (optionSelected)="userSelected($event)"> + <mat-option *ngFor="let user of filteredUsers | async" [value]="user"> + {{user.username}} + </mat-option> + </mat-autocomplete> + </mat-form-field> + </div> + </form> + </div> + </div> + <mat-divider></mat-divider> + <div class="sp-dialog-actions"> + <div fxLayout="row"> + <button mat-button mat-raised-button color="accent" (click)="save()" style="margin-right:10px;" + [disabled]="!parentForm.valid" + data-cy="sp-element-edit-user-save"> + <i class="material-icons">save</i><span> Save</span> + </button> + <button mat-button mat-raised-button class="mat-basic" (click)="close(false)"> + Cancel + </button> + </div> + </div> +</div> diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/PermissionResource.java b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.scss similarity index 83% copy from streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/PermissionResource.java copy to ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.scss index 3c8cfd7..6629d35 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/admin/PermissionResource.java +++ b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.scss @@ -15,12 +15,14 @@ * limitations under the License. * */ -package org.apache.streampipes.rest.impl.admin; -import javax.ws.rs.Path; +@import '../../../scss/sp/sp-dialog.scss'; -@Path("/v2/admin/permissions") -public class PermissionResource { +.form-field .mat-form-field-wrapper { + margin-bottom: -1.25em; +} +.form-field .mat-form-field-infix { + border-top: 0; } 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 new file mode 100644 index 0000000..a9c8961 --- /dev/null +++ b/ui/src/app/core-ui/object-permission-dialog/object-permission-dialog.component.ts @@ -0,0 +1,165 @@ +/* + * 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 { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { DialogRef } from '../dialog/base-dialog/dialog-ref'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { PermissionsService } from '../../platform-services/apis/permissions.service'; +import { + Group, + Permission, + PermissionEntry, + ServiceAccount, + UserAccount +} from '../../core-model/gen/streampipes-model-client'; +import { UserService } from '../../platform-services/apis/user.service'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { Observable, zip } from 'rxjs'; +import { UserGroupService } from '../../platform-services/apis/user-group.service'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { map, startWith } from 'rxjs/operators'; + +@Component({ + selector: 'sp-object-permission-dialog', + templateUrl: './object-permission-dialog.component.html', + styleUrls: ['./object-permission-dialog.component.scss'], +}) +export class ObjectPermissionDialogComponent implements OnInit { + + separatorKeysCodes: number[] = [ENTER, COMMA]; + + @Input() + objectInstanceId: string; + + @Input() + headerTitle: string; + + parentForm: FormGroup; + + permission: Permission; + + owner: UserAccount | ServiceAccount; + grantedUserAuthorities: (UserAccount | ServiceAccount)[]; + grantedGroupAuthorities: Group[]; + + allUsers: (UserAccount | ServiceAccount)[]; + allGroups: Group[]; + + filteredUsers: Observable<(UserAccount | ServiceAccount)[]>; + + usersLoaded = false; + + @ViewChild('userInput') fruitInput: ElementRef<HTMLInputElement>; + userCtrl = new FormControl(); + + + constructor( + private fb: FormBuilder, + private dialogRef: DialogRef<ObjectPermissionDialogComponent>, + private permissionsService: PermissionsService, + private userService: UserService, + private groupService: UserGroupService + ) { + this.grantedGroupAuthorities = []; + this.grantedUserAuthorities = []; + } + + ngOnInit(): void { + this.loadUsersAndGroups(); + this.parentForm = this.fb.group({}); + + this.parentForm.valueChanges.subscribe(v => { + this.permission.publicElement = v.publicElement; + //this.permission.ownerSid = v.owner; + }); + } + + loadUsersAndGroups() { + zip(this.userService.getAllUserAccounts(), + this.userService.getAllServiceAccounts(), + this.groupService.getAllUserGroups(), + this.permissionsService.getPermissionsForObject(this.objectInstanceId)) + .subscribe(results => { + this.allUsers = results[0]; + this.allUsers.concat(results[1]); + this.allGroups = results[2]; + this.processPermissions(results[3]); + this.usersLoaded = true; + console.log(this.allUsers); + }); + } + + processPermissions(permissions: Permission[]) { + if (permissions.length > 0) { + this.permission = permissions[0]; + this.parentForm.addControl('publicElement', new FormControl(this.permission.publicElement, Validators.required)); + this.parentForm.addControl('owner', new FormControl(this.permission.ownerSid, Validators.required)); + this.parentForm.addControl('userSelection', new FormControl(this.permission.ownerSid)); + this.filteredUsers = this.userCtrl.valueChanges.pipe( + startWith(null), + map((fruit: UserAccount | ServiceAccount | null) => fruit ? this._filter(fruit) : this.allUsers.slice())); + + this.permission.grantedAuthorities.forEach(authority => { + if (authority.principalType === 'GROUP') { + this.addGroupToSelection(authority); + } else { + this.addUserToSelection(authority); + } + }); + + } else { + console.log('No permission entry found for item'); + } + } + + save() { + + } + + close(refresh: boolean) { + this.dialogRef.close(refresh); + } + + removeUser(user: UserAccount | ServiceAccount) { + const currentIndex = this.grantedUserAuthorities.findIndex(u => u.principalId === user.principalId); + this.grantedUserAuthorities.splice(currentIndex, 1); + } + + addUser(event: MatChipInputEvent) { + event.chipInput.clear(); + } + + userSelected(event: MatAutocompleteSelectedEvent) { + this.grantedUserAuthorities.push(event.option.value); + } + + private addUserToSelection(authority: PermissionEntry) { + const user = this.allUsers.find(u => u.principalId === authority.sid); + this.grantedUserAuthorities.push(user); + } + + private addGroupToSelection(authority: PermissionEntry) { + + } + + private _filter(value: any): (UserAccount | ServiceAccount)[] { + const filterValue = value.toLowerCase(); + return this.allUsers.filter(u => u.username.toLowerCase().startsWith(filterValue)); + } +} 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 d243744..429e5a8 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 @@ -22,18 +22,19 @@ <th mat-header-cell mat-sort-header *matHeaderCellDef> Status</th> <td mat-cell *matCellDef="let pipeline"> <div fxLayout="row" fxLayoutAlign="start center"> - <div *ngIf="pipeline.running && pipeline.healthStatus == 'OK'" class="light light-green"></div> - <div *ngIf="pipeline.running && pipeline.healthStatus == 'REQUIRES_ATTENTION'" class="light light-yellow"></div> - <div *ngIf="pipeline.running && pipeline.healthStatus == 'FAILURE'" class="light light-red"></div> - <div *ngIf="!pipeline.running" class="light light-neutral" [matTooltip]="'Stopped'"></div> - <button class="ml-10" - mat-button - mat-icon-button - (click)="openPipelineNotificationsDialog(pipeline)" - *ngIf="pipeline.pipelineNotifications.length > 0" - matTooltip="{{pipeline.pipelineNotifications.length}} notification{{pipeline.pipelineNotifications.length > 1 ? 's' : ''}}"> - <mat-icon color="warn">warning</mat-icon> - </button> + <div *ngIf="pipeline.running && pipeline.healthStatus == 'OK'" class="light light-green"></div> + <div *ngIf="pipeline.running && pipeline.healthStatus == 'REQUIRES_ATTENTION'" + class="light light-yellow"></div> + <div *ngIf="pipeline.running && pipeline.healthStatus == 'FAILURE'" class="light light-red"></div> + <div *ngIf="!pipeline.running" class="light light-neutral" [matTooltip]="'Stopped'"></div> + <button class="ml-10" + mat-button + mat-icon-button + (click)="openPipelineNotificationsDialog(pipeline)" + *ngIf="pipeline.pipelineNotifications.length > 0" + matTooltip="{{pipeline.pipelineNotifications.length}} notification{{pipeline.pipelineNotifications.length > 1 ? 's' : ''}}"> + <mat-icon color="warn">warning</mat-icon> + </button> </div> </td> </ng-container> @@ -73,25 +74,36 @@ <td mat-cell *matCellDef="let pipeline"> <div fxLayout="row"> <span fxFlex fxFlexOrder="1" fxLayout="row" fxLayoutAlign="center center"> - <button color="accent" mat-button mat-icon-button matTooltip="Show pipeline" matTooltipPosition="above" + <button color="accent" mat-button mat-icon-button matTooltip="Show pipeline" + matTooltipPosition="above" (click)="pipelineOperationsService.showPipelineDetails(pipeline._id)"><i class="material-icons">search</i> </button> </span> <span fxFlex fxFlexOrder="2" fxLayout="row" fxLayoutAlign="center center"> - <button color="accent" mat-button mat-icon-button matTooltip="Modify pipeline" matTooltipPosition="above" + <button color="accent" mat-button mat-icon-button matTooltip="Modify pipeline" + matTooltipPosition="above" [disabled]="pipeline.running" (click)="pipelineOperationsService.modifyPipeline(pipeline._id)"> <i class="material-icons">mode_edit</i> </button> </span> <span fxFlex fxFlexOrder="3" fxLayout="row" fxLayoutAlign="center center"> - <button color="accent" mat-button mat-icon-button matTooltip="Delete pipeline" matTooltipPosition="above" + <button color="accent" mat-button mat-icon-button matTooltip="Delete pipeline" + matTooltipPosition="above" (click)="pipelineOperationsService.showDeleteDialog(pipeline, refreshPipelinesEmitter)" - data-cy="delete" > + data-cy="delete"> <i class="material-icons">delete</i> </button> </span> + <span fxFlex fxFlexOrder="4" fxLayout="row" fxLayoutAlign="center center" *ngIf="isAdmin"> + <button color="accent" mat-button mat-icon-button matTooltip="Permissions" + matTooltipPosition="above" + (click)="pipelineOperationsService.showPermissionsDialog(pipeline, refreshPipelinesEmitter)" + data-cy="delete"> + <i class="material-icons">share</i> + </button> + </span> </div> </td> </ng-container> diff --git a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts index a63da8f..bcaeb24 100644 --- a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts +++ b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.ts @@ -22,6 +22,8 @@ import { PipelineOperationsService } from '../../services/pipeline-operations.se import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; +import { AuthService } from '../../../services/auth.service'; +import { UserRole } from '../../../_enums/user-role.enum'; @Component({ @@ -54,12 +56,18 @@ export class PipelineOverviewComponent implements OnInit { starting: any; stopping: any; - constructor(public pipelineOperationsService: PipelineOperationsService) { + isAdmin = false; + + constructor(public pipelineOperationsService: PipelineOperationsService, + private authService: AuthService) { this.starting = false; this.stopping = false; } ngOnInit() { + this.authService.user$.subscribe(user => { + this.isAdmin = user.roles.indexOf(UserRole.ROLE_ADMIN) > -1; + }); this.toggleRunningOperation = this.toggleRunningOperation.bind(this); if (this.pipelineToStart) { diff --git a/ui/src/app/pipelines/services/pipeline-operations.service.ts b/ui/src/app/pipelines/services/pipeline-operations.service.ts index 5fce7f3..41da3f8 100644 --- a/ui/src/app/pipelines/services/pipeline-operations.service.ts +++ b/ui/src/app/pipelines/services/pipeline-operations.service.ts @@ -16,18 +16,19 @@ * */ -import {ShepherdService} from "../../services/tour/shepherd.service"; -import {EventEmitter, Injectable} from "@angular/core"; -import {PipelineService} from "../../platform-services/apis/pipeline.service"; -import {PanelType} from "../../core-ui/dialog/base-dialog/base-dialog.model"; -import {DialogService} from "../../core-ui/dialog/base-dialog/base-dialog.service"; -import {PipelineStatusDialogComponent} from "../dialog/pipeline-status/pipeline-status-dialog.component"; -import {Pipeline} from "../../core-model/gen/streampipes-model"; -import {DeletePipelineDialogComponent} from "../dialog/delete-pipeline/delete-pipeline-dialog.component"; -import {DialogRef} from "../../core-ui/dialog/base-dialog/dialog-ref"; -import {Router} from "@angular/router"; -import {PipelineAction} from "../model/pipeline-model"; -import {PipelineNotificationsComponent} from "../dialog/pipeline-notifications/pipeline-notifications.component"; +import { ShepherdService } from '../../services/tour/shepherd.service'; +import { EventEmitter, Injectable } from '@angular/core'; +import { PipelineService } from '../../platform-services/apis/pipeline.service'; +import { PanelType } from '../../core-ui/dialog/base-dialog/base-dialog.model'; +import { DialogService } from '../../core-ui/dialog/base-dialog/base-dialog.service'; +import { PipelineStatusDialogComponent } from '../dialog/pipeline-status/pipeline-status-dialog.component'; +import { Pipeline } from '../../core-model/gen/streampipes-model'; +import { DeletePipelineDialogComponent } from '../dialog/delete-pipeline/delete-pipeline-dialog.component'; +import { DialogRef } from '../../core-ui/dialog/base-dialog/dialog-ref'; +import { Router } from '@angular/router'; +import { PipelineAction } from '../model/pipeline-model'; +import { PipelineNotificationsComponent } from '../dialog/pipeline-notifications/pipeline-notifications.component'; +import { ObjectPermissionDialogComponent } from '../../core-ui/object-permission-dialog/object-permission-dialog.component'; @Injectable() export class PipelineOperationsService { @@ -36,20 +37,20 @@ export class PipelineOperationsService { stopping: any; constructor( - private ShepherdService: ShepherdService, - private PipelineService: PipelineService, - private DialogService: DialogService, - private Router: Router) { + private shepherdService: ShepherdService, + private pipelineService: PipelineService, + private dialogService: DialogService, + private router: Router) { } startPipeline(pipelineId: string, refreshPipelinesEmitter: EventEmitter<boolean>, - toggleRunningOperation?,) { + toggleRunningOperation?, ) { if (toggleRunningOperation) { toggleRunningOperation('starting'); } - let dialogRef = this.showPipelineOperationsDialog(pipelineId, PipelineAction.Start); - this.afterPipelineOperationsDialogClosed(dialogRef, refreshPipelinesEmitter, "starting", toggleRunningOperation); + const dialogRef = this.showPipelineOperationsDialog(pipelineId, PipelineAction.Start); + this.afterPipelineOperationsDialogClosed(dialogRef, refreshPipelinesEmitter, 'starting', toggleRunningOperation); } stopPipeline(pipelineId: string, @@ -58,8 +59,8 @@ export class PipelineOperationsService { if (toggleRunningOperation) { toggleRunningOperation('stopping'); } - let dialogRef = this.showPipelineOperationsDialog(pipelineId, PipelineAction.Stop); - this.afterPipelineOperationsDialogClosed(dialogRef, refreshPipelinesEmitter, "stopping", toggleRunningOperation); + const dialogRef = this.showPipelineOperationsDialog(pipelineId, PipelineAction.Stop); + this.afterPipelineOperationsDialogClosed(dialogRef, refreshPipelinesEmitter, 'stopping', toggleRunningOperation); } afterPipelineOperationsDialogClosed(dialogRef: DialogRef<PipelineStatusDialogComponent>, @@ -77,12 +78,12 @@ export class PipelineOperationsService { showDeleteDialog(pipeline: Pipeline, refreshPipelinesEmitter: EventEmitter<boolean>, switchToPipelineView?: any) { - let dialogRef: DialogRef<DeletePipelineDialogComponent> = this.DialogService.open(DeletePipelineDialogComponent, { + const dialogRef: DialogRef<DeletePipelineDialogComponent> = this.dialogService.open(DeletePipelineDialogComponent, { panelType: PanelType.STANDARD_PANEL, - title: "Delete Pipeline", - width: "70vw", + title: 'Delete Pipeline', + width: '70vw', data: { - "pipeline": pipeline, + 'pipeline': pipeline, } }); @@ -94,30 +95,30 @@ export class PipelineOperationsService { switchToPipelineView(); } } - }) - }; + }); + } showPipelineOperationsDialog(pipelineId: string, action: PipelineAction): DialogRef<PipelineStatusDialogComponent> { - return this.DialogService.open(PipelineStatusDialogComponent, { + return this.dialogService.open(PipelineStatusDialogComponent, { panelType: PanelType.STANDARD_PANEL, - title: "Pipeline Status", - width: "70vw", + title: 'Pipeline Status', + width: '70vw', data: { - "pipelineId": pipelineId, - "action": action + 'pipelineId': pipelineId, + 'action': action } }); - }; + } showPipelineNotificationsDialog(pipeline: Pipeline, refreshPipelinesEmitter: EventEmitter<boolean>) { - let dialogRef: DialogRef<PipelineNotificationsComponent> = this.DialogService.open(PipelineNotificationsComponent, { + const dialogRef: DialogRef<PipelineNotificationsComponent> = this.dialogService.open(PipelineNotificationsComponent, { panelType: PanelType.STANDARD_PANEL, - title: "Pipeline Notifications", - width: "70vw", + title: 'Pipeline Notifications', + width: '70vw', data: { - "pipeline": pipeline, + 'pipeline': pipeline, } }); @@ -126,12 +127,29 @@ export class PipelineOperationsService { }); } + showPermissionsDialog(pipeline: Pipeline, + refreshPipelinesEmitter: EventEmitter<boolean>) { + const dialogRef = this.dialogService.open(ObjectPermissionDialogComponent, { + panelType: PanelType.SLIDE_IN_PANEL, + title: 'Manage permissions', + width: '70vw', + data: { + 'objectInstanceId': pipeline._id, + 'headerTitle': 'Manage permissions for pipeline ' + pipeline.name + } + }); + + dialogRef.afterClosed().subscribe(refresh => { + refreshPipelinesEmitter.emit(refresh); + }); + } + showPipelineInEditor(id) { - this.Router.navigate(["editor"], { queryParams: { pipeline: id }}); + this.router.navigate(['editor'], { queryParams: { pipeline: id }}); } showPipelineDetails(id) { - this.Router.navigate(["pipeline-details"], { queryParams: { pipeline: id }}); + this.router.navigate(['pipeline-details'], { queryParams: { pipeline: id }}); } modifyPipeline(pipeline) { @@ -139,6 +157,6 @@ export class PipelineOperationsService { } showLogs(id) { - //this.$state.go("streampipes.pipelinelogs", {pipeline: id}); + // this.$state.go("streampipes.pipelinelogs", {pipeline: id}); } } diff --git a/ui/src/app/platform-services/apis/permissions.service.ts b/ui/src/app/platform-services/apis/permissions.service.ts new file mode 100644 index 0000000..4c87050 --- /dev/null +++ b/ui/src/app/platform-services/apis/permissions.service.ts @@ -0,0 +1,48 @@ +/* + * 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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PlatformServicesCommons } from './commons.service'; +import { Observable } from 'rxjs'; +import { + Message, + Pipeline, + PipelineCategory, + PipelineOperationStatus, + PipelineStatusMessage +} from '../../core-model/gen/streampipes-model'; +import { map } from 'rxjs/operators'; +import { Permission } from '../../core-model/gen/streampipes-model-client'; + +@Injectable() +export class PermissionsService { + + constructor(private http: HttpClient, + private platformServicesCommons: PlatformServicesCommons) { } + + + public getPermissionsForObject(objectInstanceId: string): Observable<Permission[]> { + return this.http.get(`${this.permissionsBasePath}/${objectInstanceId}`).pipe(map(response => response as Permission[])); + } + + + get permissionsBasePath() { + return `${this.platformServicesCommons.apiBasePath}/admin/permissions`; + } +} diff --git a/ui/src/app/platform-services/apis/user.service.ts b/ui/src/app/platform-services/apis/user.service.ts index 56e9979..1eabf19 100644 --- a/ui/src/app/platform-services/apis/user.service.ts +++ b/ui/src/app/platform-services/apis/user.service.ts @@ -21,7 +21,11 @@ import { HttpClient } from '@angular/common/http'; import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { PlatformServicesCommons } from './commons.service'; -import { ServiceAccount, UserAccount } from '../../core-model/gen/streampipes-model-client'; +import { + Principal, + ServiceAccount, + UserAccount +} from '../../core-model/gen/streampipes-model-client'; @Injectable() export class UserService { @@ -64,6 +68,17 @@ export class UserService { return this.http.delete(`${this.usersPath}/${principalId}`); } + public getUserById(principalId: string): Observable<UserAccount | ServiceAccount> { + return this.http.get(`${this.usersPath}/${principalId}`) + .pipe(map(response => { + if ((response as any).principalType === 'USER_ACCOUNT') { + return UserAccount.fromData(response as any); + } else { + return ServiceAccount.fromData(response as any); + } + })); + } + private get usersPath() { return this.platformServicesCommons.apiBasePath + '/users'; } diff --git a/ui/src/app/platform-services/platform.module.ts b/ui/src/app/platform-services/platform.module.ts index cb55ffb..59c23b2 100644 --- a/ui/src/app/platform-services/platform.module.ts +++ b/ui/src/app/platform-services/platform.module.ts @@ -31,15 +31,16 @@ import { PipelineTemplateService } from './apis/pipeline-template.service'; import { UserService } from './apis/user.service'; import { UserGroupService } from './apis/user-group.service'; import { MailConfigService } from './apis/mail-config.service'; +import { PermissionsService } from './apis/permissions.service'; @NgModule({ - imports: [ - ], + imports: [], declarations: [], providers: [ FilesService, MailConfigService, MeasurementUnitsService, + PermissionsService, PlatformServicesCommons, PipelineCanvasMetadataService, PipelineElementEndpointService, diff --git a/ui/src/app/services/auth.service.ts b/ui/src/app/services/auth.service.ts index 42fe5e3..dbc116f 100644 --- a/ui/src/app/services/auth.service.ts +++ b/ui/src/app/services/auth.service.ts @@ -32,7 +32,7 @@ import { RoleModel } from '../_models/auth.model'; export class AuthService { public authToken$: BehaviorSubject<string> = new BehaviorSubject<string>(undefined); - public user$: BehaviorSubject<any> = new BehaviorSubject<any>(undefined); + public user$: BehaviorSubject<UserInfo> = new BehaviorSubject<UserInfo>(undefined); public isLoggedIn$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); public darkMode$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); @@ -56,7 +56,6 @@ export class AuthService { const jwtHelper: JwtHelperService = new JwtHelperService({}); const decodedToken = jwtHelper.decodeToken(data.accessToken); this.tokenStorage.saveToken(data.accessToken); - console.log(decodedToken.user); this.tokenStorage.saveUser(decodedToken.user); this.authToken$.next(data.accessToken); this.user$.next(decodedToken.user);
