This is an automated email from the ASF dual-hosted git repository.
zehnder pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to refs/heads/dev by this push:
new cfbcaac8b8 Add dataset permissions (#4099)
cfbcaac8b8 is described below
commit cfbcaac8b854e9eb329946bf5edd5e4144641aa4
Author: Dominik Riemer <[email protected]>
AuthorDate: Tue Jan 20 15:23:39 2026 +0100
Add dataset permissions (#4099)
Co-authored-by: Jacqueline Höllig <[email protected]>
Co-authored-by: Jacqueline Höllig
<[email protected]>
Co-authored-by: Philipp Zehnder <[email protected]>
---
.../streampipes/client/api/IStreamPipesClient.java | 2 +
.../api/config/IStreamPipesClientConfig.java | 8 +
.../streampipes/client/StreamPipesClient.java | 10 +-
.../streampipes/client/http/HttpRequest.java | 6 +-
.../client/model/StreamPipesClientConfig.java | 18 +
.../commons/constants/HttpConstants.java | 1 +
.../api/IDataExplorerSchemaManagement.java | 2 +-
.../influx/DataExplorerManagerInflux.java | 7 +-
.../iotdb/DataExplorerManagerIotDb.java | 7 +-
.../dataexplorer/DataExplorerSchemaManagement.java | 13 +-
.../DataExplorerSchemaManagementTest.java | 24 +-
.../sinks/internal/jvm/datalake/DataLakeSink.java | 5 +-
.../model/client/user/DefaultPrivilege.java | 7 +
.../permission/DataLakePermissionManager.java | 46 ++
.../manager/storage/PipelineStorageService.java | 2 +-
.../impl/datalake/AbstractDataLakeResource.java | 80 +++
.../impl/datalake/DataLakeMeasureResource.java | 28 +-
.../rest/impl/datalake/DataLakeResource.java | 47 +-
.../impl/datalake/PersistedDataStreamResource.java | 4 +-
.../streampipes/rest/security/AuthConstants.java | 7 +
.../core/filter/TokenAuthenticationFilter.java | 23 +-
.../core/migrations/AvailableMigrations.java | 6 +-
.../v099/CreateAssetPermissionMigration.java | 3 +-
.../v099/CreateDatasetPermissionMigration.java | 118 +++++
.../v099/RemoveDuplicatedAssetPermissions.java | 106 ++++
.../management/authorization/PrivilegeManager.java | 4 +-
.../user/management/authorization/RoleManager.java | 19 +-
.../support/utils/dataExplorer/DataExplorerBtns.ts | 8 +
.../utils/dataExplorer/DataExplorerUtils.ts | 7 +
ui/cypress/support/utils/dataset/DatasetBtns.ts | 23 +
ui/cypress/support/utils/dataset/DatasetUtils.ts | 44 ++
.../userManagement/testUserRoleDataset.spec.ts | 221 ++++++++
ui/deployment/modules.yml | 2 +-
.../src/lib/model/gen/streampipes-model-client.ts | 7 +-
.../src/lib/model/gen/streampipes-model.ts | 3 +-
.../sp-exception-message.component.html | 1 +
ui/src/app/_enums/user-privilege.enum.ts | 3 +
.../chart-overview-table.component.html | 12 +-
.../file-overview/file-overview.component.html | 15 +-
.../dashboard-toolbar.component.html | 1 +
.../datalake-configuration.component.html | 581 +++++++++++----------
.../datalake-configuration.component.ts | 28 +
42 files changed, 1223 insertions(+), 336 deletions(-)
diff --git
a/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/IStreamPipesClient.java
b/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/IStreamPipesClient.java
index 44f5064431..90b37b5a1d 100644
---
a/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/IStreamPipesClient.java
+++
b/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/IStreamPipesClient.java
@@ -61,4 +61,6 @@ public interface IStreamPipesClient extends Serializable {
IFileApi fileApi();
IDataLakeResourceApi dataLakeResourceApi();
+
+ IStreamPipesClient onBehalfOf(String userSid);
}
diff --git
a/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/config/IStreamPipesClientConfig.java
b/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/config/IStreamPipesClientConfig.java
index 8f49b487e1..5f71eba926 100644
---
a/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/config/IStreamPipesClientConfig.java
+++
b/streampipes-client-api/src/main/java/org/apache/streampipes/client/api/config/IStreamPipesClientConfig.java
@@ -20,9 +20,17 @@ package org.apache.streampipes.client.api.config;
import org.apache.streampipes.messaging.SpProtocolDefinitionFactory;
+import org.apache.http.Header;
+
+import java.util.Set;
+
public interface IStreamPipesClientConfig {
void addTransportProtocol(SpProtocolDefinitionFactory<?>
protocolDefinitionFactory);
+ void addCustomHeader(String name, String value);
+
+ Set<Header> getCustomHeaders();
+
ClientConnectionUrlResolver getConnectionConfig();
}
diff --git
a/streampipes-client/src/main/java/org/apache/streampipes/client/StreamPipesClient.java
b/streampipes-client/src/main/java/org/apache/streampipes/client/StreamPipesClient.java
index d159809b1e..9870a73054 100644
---
a/streampipes-client/src/main/java/org/apache/streampipes/client/StreamPipesClient.java
+++
b/streampipes-client/src/main/java/org/apache/streampipes/client/StreamPipesClient.java
@@ -39,6 +39,7 @@ import
org.apache.streampipes.client.api.credentials.CredentialsProvider;
import org.apache.streampipes.client.model.StreamPipesClientConfig;
import org.apache.streampipes.client.model.StreamPipesClientConnectionConfig;
import org.apache.streampipes.client.paths.ApiPath;
+import org.apache.streampipes.commons.constants.HttpConstants;
import org.apache.streampipes.messaging.SpProtocolDefinitionFactory;
import org.apache.streampipes.model.mail.SpEmail;
@@ -47,7 +48,7 @@ public class StreamPipesClient implements
private static final Integer SP_DEFAULT_PORT = 80;
- private StreamPipesClientConfig config;
+ private final StreamPipesClientConfig config;
private StreamPipesClient(ClientConnectionUrlResolver connectionConfig) {
this.config = new StreamPipesClientConfig(connectionConfig);
@@ -231,4 +232,11 @@ public class StreamPipesClient implements
public DataLakeResourceApi dataLakeResourceApi () {
return new DataLakeResourceApi (config);
}
+
+ @Override
+ public IStreamPipesClient onBehalfOf(String userSid) {
+ var scoped = new StreamPipesClient(config.getConnectionConfig());
+ scoped.config.addCustomHeader(HttpConstants.X_ON_BEHALF_OF, userSid);
+ return scoped;
+ }
}
diff --git
a/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
b/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
index 75afd01750..cbcc8550e5 100644
---
a/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
+++
b/streampipes-client/src/main/java/org/apache/streampipes/client/http/HttpRequest.java
@@ -63,11 +63,15 @@ public abstract class HttpRequest<K, V, T> {
protected Header[] standardJsonHeaders() {
List<Header> headers = new
ArrayList<>(connectionConfig.getCredentials().makeHeaders());
headers.add(Headers.acceptJson());
+ headers.addAll(clientConfig.getCustomHeaders());
return headers.toArray(new Header[0]);
}
protected Header[] standardHeaders() {
- List<Header> headers = new
ArrayList<>(connectionConfig.getCredentials().makeHeaders());
+ List<Header> headers = new ArrayList<>();
+ headers.addAll(connectionConfig.getCredentials().makeHeaders());
+ headers.addAll(clientConfig.getCustomHeaders());
+
return headers.toArray(new Header[0]);
}
diff --git
a/streampipes-client/src/main/java/org/apache/streampipes/client/model/StreamPipesClientConfig.java
b/streampipes-client/src/main/java/org/apache/streampipes/client/model/StreamPipesClientConfig.java
index 92e9426587..083877ce85 100644
---
a/streampipes-client/src/main/java/org/apache/streampipes/client/model/StreamPipesClientConfig.java
+++
b/streampipes-client/src/main/java/org/apache/streampipes/client/model/StreamPipesClientConfig.java
@@ -22,12 +22,20 @@ import
org.apache.streampipes.client.api.config.IStreamPipesClientConfig;
import org.apache.streampipes.messaging.SpProtocolDefinitionFactory;
import org.apache.streampipes.messaging.SpProtocolManager;
+import org.apache.http.Header;
+import org.apache.http.message.BasicHeader;
+
+import java.util.HashSet;
+import java.util.Set;
+
public class StreamPipesClientConfig implements IStreamPipesClientConfig {
private final ClientConnectionUrlResolver connectionConfig;
+ private final Set<Header> customHeaders;
public StreamPipesClientConfig(ClientConnectionUrlResolver connectionConfig)
{
this.connectionConfig = connectionConfig;
+ this.customHeaders = new HashSet<>();
}
@Override
@@ -35,6 +43,16 @@ public class StreamPipesClientConfig implements
IStreamPipesClientConfig {
SpProtocolManager.INSTANCE.register(protocolDefinitionFactory);
}
+ @Override
+ public void addCustomHeader(String name, String value) {
+ customHeaders.add(new BasicHeader(name, value));
+ }
+
+ @Override
+ public Set<Header> getCustomHeaders() {
+ return customHeaders;
+ }
+
@Override
public ClientConnectionUrlResolver getConnectionConfig() {
return connectionConfig;
diff --git
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/HttpConstants.java
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/HttpConstants.java
index 9866f10e82..22ce5b0c02 100644
---
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/HttpConstants.java
+++
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/constants/HttpConstants.java
@@ -26,6 +26,7 @@ public class HttpConstants {
public static final String APPLICATION_JSON_TYPE = "application/json";
public static final String X_API_USER = "X-API-USER";
public static final String X_API_KEY = "X-API-KEY";
+ public static final String X_ON_BEHALF_OF = "X-On-Behalf-Of";
public static final String BASIC = "Basic ";
}
diff --git
a/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
b/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
index c171469c52..e8efa046da 100644
---
a/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
+++
b/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataExplorerSchemaManagement.java
@@ -31,7 +31,7 @@ public interface IDataExplorerSchemaManagement {
Optional<DataLakeMeasure> getExistingMeasureByName(String measureName);
- DataLakeMeasure createOrUpdateMeasurement(DataLakeMeasure measure);
+ DataLakeMeasure createOrUpdateMeasurement(DataLakeMeasure measure,String
principalSid);
void deleteMeasurement(String elementId);
diff --git
a/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataExplorerManagerInflux.java
b/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataExplorerManagerInflux.java
index f88cf6c585..5ec67f0db9 100644
---
a/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataExplorerManagerInflux.java
+++
b/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataExplorerManagerInflux.java
@@ -29,6 +29,7 @@ import
org.apache.streampipes.dataexplorer.api.IDataLakeMeasurementSanitizer;
import org.apache.streampipes.dataexplorer.api.ITimeSeriesStorage;
import org.apache.streampipes.dataexplorer.influx.client.InfluxClientProvider;
import
org.apache.streampipes.dataexplorer.influx.sanitize.DataLakeMeasurementSanitizerInflux;
+import org.apache.streampipes.manager.permission.DataLakePermissionManager;
import org.apache.streampipes.model.datalake.DataLakeMeasure;
import org.apache.streampipes.storage.management.StorageDispatcher;
@@ -55,7 +56,11 @@ public class DataExplorerManagerInflux implements
IDataExplorerManager {
public IDataExplorerSchemaManagement getSchemaManagement() {
return new DataExplorerSchemaManagement(StorageDispatcher.INSTANCE
.getNoSqlStore()
- .getDataLakeStorage());
+ .getDataLakeStorage(),
+ new DataLakePermissionManager(
+ StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage()
+ )
+ );
}
@Override
diff --git
a/streampipes-data-explorer-iotdb/src/main/java/org/apache/streampipes/dataexplorer/iotdb/DataExplorerManagerIotDb.java
b/streampipes-data-explorer-iotdb/src/main/java/org/apache/streampipes/dataexplorer/iotdb/DataExplorerManagerIotDb.java
index e931403809..ba0f060b94 100644
---
a/streampipes-data-explorer-iotdb/src/main/java/org/apache/streampipes/dataexplorer/iotdb/DataExplorerManagerIotDb.java
+++
b/streampipes-data-explorer-iotdb/src/main/java/org/apache/streampipes/dataexplorer/iotdb/DataExplorerManagerIotDb.java
@@ -28,6 +28,7 @@ import
org.apache.streampipes.dataexplorer.api.IDataLakeMeasurementCounter;
import org.apache.streampipes.dataexplorer.api.IDataLakeMeasurementSanitizer;
import org.apache.streampipes.dataexplorer.api.ITimeSeriesStorage;
import
org.apache.streampipes.dataexplorer.iotdb.sanitize.DataLakeMeasurementSanitizerIotDb;
+import org.apache.streampipes.manager.permission.DataLakePermissionManager;
import org.apache.streampipes.model.datalake.DataLakeMeasure;
import org.apache.streampipes.storage.management.StorageDispatcher;
@@ -54,7 +55,11 @@ public class DataExplorerManagerIotDb implements
IDataExplorerManager {
public IDataExplorerSchemaManagement getSchemaManagement() {
return new DataExplorerSchemaManagement(StorageDispatcher.INSTANCE
.getNoSqlStore()
-
.getDataLakeStorage());
+
.getDataLakeStorage(),
+ new DataLakePermissionManager(
+ StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage()
+ )
+ );
}
@Override
diff --git
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
index 718dcba4af..c9a5c9db78 100644
---
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
+++
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagement.java
@@ -20,6 +20,7 @@ package org.apache.streampipes.dataexplorer;
import org.apache.streampipes.dataexplorer.api.IDataExplorerSchemaManagement;
import org.apache.streampipes.dataexplorer.utils.DataExplorerUtils;
+import org.apache.streampipes.manager.permission.DataLakePermissionManager;
import org.apache.streampipes.model.datalake.DataLakeMeasure;
import
org.apache.streampipes.model.datalake.DataLakeMeasureSchemaUpdateStrategy;
import org.apache.streampipes.model.schema.EventProperty;
@@ -28,6 +29,7 @@ import org.apache.streampipes.storage.api.CRUDStorage;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -35,9 +37,12 @@ import java.util.stream.Stream;
public class DataExplorerSchemaManagement implements
IDataExplorerSchemaManagement {
CRUDStorage<DataLakeMeasure> dataLakeStorage;
+ private final DataLakePermissionManager permissionManager;
- public DataExplorerSchemaManagement(CRUDStorage<DataLakeMeasure>
dataLakeStorage) {
+ public DataExplorerSchemaManagement(CRUDStorage<DataLakeMeasure>
dataLakeStorage,
+ DataLakePermissionManager
permissionManager) {
this.dataLakeStorage = dataLakeStorage;
+ this.permissionManager = permissionManager;
}
@Override
@@ -56,14 +61,18 @@ public class DataExplorerSchemaManagement implements
IDataExplorerSchemaManageme
* according to the update strategy defined by the measurement.
*/
@Override
- public DataLakeMeasure createOrUpdateMeasurement(DataLakeMeasure measure) {
+ public DataLakeMeasure createOrUpdateMeasurement(DataLakeMeasure measure,
+ String principalSid) {
setDefaultUpdateStrategyIfNoneProvided(measure);
var existingMeasure = getExistingMeasureByName(measure.getMeasureName());
if (existingMeasure.isEmpty()) {
+ measure.setElementId(UUID.randomUUID().toString());
setSchemaVersionAndStoreMeasurement(measure);
+
permissionManager.makeAndPersistDataLakePermission(measure.getElementId(),
principalSid);
+
} else {
handleExistingMeasurement(measure, existingMeasure.get());
}
diff --git
a/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
b/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
index 4dc34531ce..a4030ef91e 100644
---
a/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
+++
b/streampipes-data-explorer/src/test/java/org/apache/streampipes/dataexplorer/DataExplorerSchemaManagementTest.java
@@ -18,10 +18,12 @@
package org.apache.streampipes.dataexplorer;
+import org.apache.streampipes.manager.permission.DataLakePermissionManager;
import org.apache.streampipes.model.datalake.DataLakeMeasure;
import
org.apache.streampipes.model.datalake.DataLakeMeasureSchemaUpdateStrategy;
import org.apache.streampipes.model.schema.EventProperty;
import org.apache.streampipes.storage.api.CRUDStorage;
+import org.apache.streampipes.storage.api.IPermissionStorage;
import org.apache.streampipes.test.generator.EventPropertyPrimitiveTestBuilder;
import org.apache.streampipes.test.generator.EventSchemaTestBuilder;
import org.apache.streampipes.vocabulary.XSD;
@@ -47,22 +49,28 @@ public class DataExplorerSchemaManagementTest {
public static final String OLD_PROPERTY = "oldProperty";
private CRUDStorage<DataLakeMeasure> dataLakeStorageMock;
+ private DataLakePermissionManager permissionManagerMock;
@BeforeEach
public void setUp() {
dataLakeStorageMock = mock(CRUDStorage.class);
+ IPermissionStorage permissionStorageMock = mock(IPermissionStorage.class);
+ this.permissionManagerMock = new
DataLakePermissionManager(permissionStorageMock);
}
@Test
public void createMeasurementThatNotExisted() {
when(dataLakeStorageMock.findAll()).thenReturn(List.of());
- var schemaManagement = new
DataExplorerSchemaManagement(dataLakeStorageMock);
+ var schemaManagement = new DataExplorerSchemaManagement(
+ dataLakeStorageMock,
+ permissionManagerMock
+ );
var oldMeasure = getSampleMeasure(
DataLakeMeasureSchemaUpdateStrategy.UPDATE_SCHEMA,
List.of()
);
- var resultingMeasure =
schemaManagement.createOrUpdateMeasurement(oldMeasure);
+ var resultingMeasure =
schemaManagement.createOrUpdateMeasurement(oldMeasure,null);
assertEquals(oldMeasure.getMeasureName(),
resultingMeasure.getMeasureName());
verify(dataLakeStorageMock, Mockito.times(1))
@@ -82,11 +90,11 @@ public class DataExplorerSchemaManagementTest {
when(dataLakeStorageMock.findAll()).thenReturn(List.of(oldMeasure));
when(dataLakeStorageMock.getElementById(any())).thenReturn(oldMeasure);
- var schemaManagement = new
DataExplorerSchemaManagement(dataLakeStorageMock);
+ var schemaManagement = new
DataExplorerSchemaManagement(dataLakeStorageMock, permissionManagerMock);
var newMeasure =
getNewMeasure(DataLakeMeasureSchemaUpdateStrategy.UPDATE_SCHEMA);
- var resultMeasure = schemaManagement.createOrUpdateMeasurement(newMeasure);
+ var resultMeasure =
schemaManagement.createOrUpdateMeasurement(newMeasure,null);
assertEquals(newMeasure.getMeasureName(), resultMeasure.getMeasureName());
verify(dataLakeStorageMock, Mockito.times(1))
@@ -108,10 +116,10 @@ public class DataExplorerSchemaManagementTest {
);
when(dataLakeStorageMock.findAll()).thenReturn(List.of(oldMeasure));
when(dataLakeStorageMock.getElementById(any())).thenReturn(oldMeasure);
- var schemaManagement = new
DataExplorerSchemaManagement(dataLakeStorageMock);
+ var schemaManagement = new
DataExplorerSchemaManagement(dataLakeStorageMock, permissionManagerMock);
var newMeasure =
getNewMeasure(DataLakeMeasureSchemaUpdateStrategy.EXTEND_EXISTING_SCHEMA);
- var resultMeasure = schemaManagement.createOrUpdateMeasurement(newMeasure);
+ var resultMeasure =
schemaManagement.createOrUpdateMeasurement(newMeasure,null);
assertEquals(newMeasure.getMeasureName(), resultMeasure.getMeasureName());
verify(dataLakeStorageMock, Mockito.times(1)).updateElement(any());
@@ -133,11 +141,11 @@ public class DataExplorerSchemaManagementTest {
when(dataLakeStorageMock.findAll()).thenReturn(List.of(oldMeasure));
when(dataLakeStorageMock.getElementById(any())).thenReturn(oldMeasure);
- var schemaManagement = new
DataExplorerSchemaManagement(dataLakeStorageMock);
+ var schemaManagement = new
DataExplorerSchemaManagement(dataLakeStorageMock, permissionManagerMock);
var newMeasure =
getNewMeasure(DataLakeMeasureSchemaUpdateStrategy.EXTEND_EXISTING_SCHEMA);
- var resultMeasure = schemaManagement.createOrUpdateMeasurement(newMeasure);
+ var resultMeasure =
schemaManagement.createOrUpdateMeasurement(newMeasure,null);
assertEquals(newMeasure.getMeasureName(), resultMeasure.getMeasureName());
verify(dataLakeStorageMock, Mockito.times(1)).updateElement(any());
assertEquals(
diff --git
a/streampipes-extensions/streampipes-sinks-internal-jvm/src/main/java/org/apache/streampipes/sinks/internal/jvm/datalake/DataLakeSink.java
b/streampipes-extensions/streampipes-sinks-internal-jvm/src/main/java/org/apache/streampipes/sinks/internal/jvm/datalake/DataLakeSink.java
index eb4565271d..0a1b5bf1ea 100644
---
a/streampipes-extensions/streampipes-sinks-internal-jvm/src/main/java/org/apache/streampipes/sinks/internal/jvm/datalake/DataLakeSink.java
+++
b/streampipes-extensions/streampipes-sinks-internal-jvm/src/main/java/org/apache/streampipes/sinks/internal/jvm/datalake/DataLakeSink.java
@@ -126,8 +126,11 @@ public class DataLakeSink implements IStreamPipesDataSink,
SupportsRuntimeConfig
measure.setSchemaUpdateStrategy(DataLakeMeasureSchemaUpdateStrategy.UPDATE_SCHEMA);
}
+ var userSid = parameters.getModel().getCorrespondingUser();
+ var client = runtimeContext.getStreamPipesClient().onBehalfOf(userSid);
+
measure = new DataExplorerDispatcher().getDataExplorerManager()
-
.getMeasurementSanitizer(runtimeContext.getStreamPipesClient(), measure)
+ .getMeasurementSanitizer(client,
measure)
.sanitizeAndRegister();
this.timeSeriesStore = new TimeSeriesStore(
diff --git
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/DefaultPrivilege.java
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/DefaultPrivilege.java
index c445fb5240..7c4014aa56 100644
---
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/DefaultPrivilege.java
+++
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/DefaultPrivilege.java
@@ -33,6 +33,10 @@ public enum DefaultPrivilege {
PRIVILEGE_READ_PIPELINE_ELEMENT(Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE),
PRIVILEGE_WRITE_PIPELINE_ELEMENT(Constants.PRIVILEGE_WRITE_PIPELINE_ELEMENT_VALUE),
+ // Datasets
+ PRIVILEGE_READ_DATASET(Constants.PRIVILEGE_READ_DATASET_VALUE),
+ PRIVILEGE_WRITE_DATASET(Constants.PRIVILEGE_WRITE_DATASET_VALUE),
+
// Dashboard
PRIVILEGE_READ_DASHBOARD(Constants.PRIVILEGE_READ_DASHBOARD_VALUE),
PRIVILEGE_WRITE_DASHBOARD(Constants.PRIVILEGE_WRITE_DASHBOARD_VALUE),
@@ -77,6 +81,9 @@ public enum DefaultPrivilege {
public static final String PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE =
"PRIVILEGE_READ_PIPELINE_ELEMENT";
public static final String PRIVILEGE_WRITE_PIPELINE_ELEMENT_VALUE =
"PRIVILEGE_WRITE_PIPELINE_ELEMENT";
+ public static final String PRIVILEGE_READ_DATASET_VALUE =
"PRIVILEGE_READ_DATASET";
+ public static final String PRIVILEGE_WRITE_DATASET_VALUE =
"PRIVILEGE_WRITE_DATASET";
+
public static final String PRIVILEGE_READ_DASHBOARD_VALUE =
"PRIVILEGE_READ_DASHBOARD";
public static final String PRIVILEGE_WRITE_DASHBOARD_VALUE =
"PRIVILEGE_WRITE_DASHBOARD";
diff --git
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/permission/DataLakePermissionManager.java
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/permission/DataLakePermissionManager.java
new file mode 100644
index 0000000000..34222c97e8
--- /dev/null
+++
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/permission/DataLakePermissionManager.java
@@ -0,0 +1,46 @@
+/*
+ * 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.manager.permission;
+
+import org.apache.streampipes.model.client.user.Permission;
+import org.apache.streampipes.model.client.user.PermissionBuilder;
+import org.apache.streampipes.model.datalake.DataLakeMeasure;
+import org.apache.streampipes.storage.api.IPermissionStorage;
+
+public class DataLakePermissionManager {
+
+ private final IPermissionStorage permissionStorage;
+
+ public DataLakePermissionManager(IPermissionStorage permissionStorage) {
+ this.permissionStorage = permissionStorage;
+ }
+
+ private Permission createDataLakePermission(String measurement, String
principalSid) {
+ return PermissionBuilder
+ .create(measurement, DataLakeMeasure.class, principalSid)
+ .build();
+ }
+
+ public void makeAndPersistDataLakePermission(String measurement,
+ String ownerSid) {
+
+ Permission p = createDataLakePermission(measurement, ownerSid);
+ permissionStorage.persist(p);
+
+ }
+}
diff --git
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/PipelineStorageService.java
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/PipelineStorageService.java
index 09709fac7b..206f819157 100644
---
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/PipelineStorageService.java
+++
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/PipelineStorageService.java
@@ -34,6 +34,7 @@ public class PipelineStorageService {
private final Pipeline pipeline;
+
public PipelineStorageService(Pipeline pipeline) {
this.pipeline = pipeline;
}
@@ -59,7 +60,6 @@ public class PipelineStorageService {
List<DataSinkInvocation> secs = filter(graphs, DataSinkInvocation.class);
List<DataProcessorInvocation> sepas = filter(graphs,
DataProcessorInvocation.class);
-
pipeline.setSepas(sepas);
pipeline.setActions(secs);
}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/AbstractDataLakeResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/AbstractDataLakeResource.java
new file mode 100644
index 0000000000..3fdc5a8998
--- /dev/null
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/AbstractDataLakeResource.java
@@ -0,0 +1,80 @@
+/*
+ * 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.rest.impl.datalake;
+
+import org.apache.streampipes.dataexplorer.api.IDataExplorerSchemaManagement;
+import org.apache.streampipes.dataexplorer.management.DataExplorerDispatcher;
+import org.apache.streampipes.model.client.user.DefaultPrivilege;
+import
org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
+import org.apache.streampipes.rest.security.SpPermissionEvaluator;
+import org.apache.streampipes.storage.api.IDataLakeMeasureStorage;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.Objects;
+
+public class AbstractDataLakeResource extends AbstractAuthGuardedRestResource {
+
+ final IDataExplorerSchemaManagement dataLakeMeasureManagement;
+ private final IDataLakeMeasureStorage dataLakeMeasureStorage =
+ StorageDispatcher.INSTANCE.getNoSqlStore().getDataLakeStorage();
+
+
+
+ public AbstractDataLakeResource() {
+ this.dataLakeMeasureManagement = new
DataExplorerDispatcher().getDataExplorerManager()
+ .getSchemaManagement();
+ }
+
+ /**
+ * required by Spring expression
+ */
+ public boolean hasReadAuthority() {
+ return
isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE);
+ }
+
+ /**
+ * required by Spring expression
+ */
+ public boolean hasWriteAuthority() {
+ return
isAdminOrHasAnyAuthority(DefaultPrivilege.Constants.PRIVILEGE_WRITE_DATASET_VALUE);
+ }
+
+ /**
+ * required by Spring expression, do not delete if IDE shows this as unused
+ * @param measurementName the name of the data lake measure
+ * @param permission read or write privilege
+ * @return true if user has permission, false if not or measure does not
exist
+ */
+ public boolean checkPermissionByName(String measurementName,
+ String permission) {
+
+ var measure = dataLakeMeasureStorage.getByMeasureName(measurementName);
+ if (Objects.nonNull(measure)) {
+ var spPermissionEvaluator = new SpPermissionEvaluator();
+ var authentication = SecurityContextHolder.getContext()
+ .getAuthentication();
+ return spPermissionEvaluator.hasPermission(
+ authentication,
+ measure.getElementId(),
+ permission);
+ }
+ return false;
+ }
+}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
index 4c36c5a06c..359efaffe2 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeMeasureResource.java
@@ -18,15 +18,15 @@
package org.apache.streampipes.rest.impl.datalake;
-import org.apache.streampipes.dataexplorer.api.IDataExplorerSchemaManagement;
import org.apache.streampipes.dataexplorer.management.DataExplorerDispatcher;
import org.apache.streampipes.model.datalake.DataLakeMeasure;
-import
org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -43,18 +43,19 @@ import java.util.Objects;
@RestController
@RequestMapping("/api/v4/datalake/measure")
-public class DataLakeMeasureResource extends AbstractAuthGuardedRestResource {
-
- private final IDataExplorerSchemaManagement dataLakeMeasureManagement;
+public class DataLakeMeasureResource extends AbstractDataLakeResource {
public DataLakeMeasureResource() {
- this.dataLakeMeasureManagement = new
DataExplorerDispatcher().getDataExplorerManager()
- .getSchemaManagement();
+ super();
}
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes =
MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasWriteAuthority()")
public ResponseEntity<DataLakeMeasure> addDataLake(@RequestBody
DataLakeMeasure dataLakeMeasure) {
- DataLakeMeasure result =
this.dataLakeMeasureManagement.createOrUpdateMeasurement(dataLakeMeasure);
+ DataLakeMeasure result =
this.dataLakeMeasureManagement.createOrUpdateMeasurement(
+ dataLakeMeasure,
+ getAuthenticatedUserSid()
+ );
return ok(result);
}
@@ -68,7 +69,9 @@ public class DataLakeMeasureResource extends
AbstractAuthGuardedRestResource {
*/
@Operation(summary = "Retrieve measurement counts", description = "Retrieves
the entry counts for the specified measurements from the data lake.")
@GetMapping(path = "/count", produces = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity<Map<String, Integer>> getEntryCountsOfMeasurments(
+ @PreAuthorize("this.hasReadAuthority()")
+ @PostFilter("this.checkPermissionByName(filterObject.key, 'READ')")
+ public Map<String, Integer> getEntryCountsOfMeasurements(
@Parameter(description = "A list of measurement names to return the
count.") @RequestParam(value = "measurementNames") List<String>
measurementNames,
@Parameter(description = "The number of days from today where the count
should start") @RequestParam(value = "daysBack", defaultValue = "-1") int
daysBack) {
var allMeasurements = this.dataLakeMeasureManagement.getAllMeasurements();
@@ -79,10 +82,11 @@ public class DataLakeMeasureResource extends
AbstractAuthGuardedRestResource {
measurementNames,
daysBack)
.countMeasurementSizes();
- return ok(result);
+ return result;
}
@GetMapping(path = "{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasReadAuthority() and hasPermission(#elementId,
'READ')")
public ResponseEntity<?> getDataLakeMeasure(@PathVariable("id") String
elementId) {
var measure = this.dataLakeMeasureManagement.getById(elementId);
if (Objects.nonNull(measure)) {
@@ -93,6 +97,7 @@ public class DataLakeMeasureResource extends
AbstractAuthGuardedRestResource {
}
@GetMapping(path = "byName/{measureName}", produces =
MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasReadAuthority() and
this.checkPermissionByName(#measureName, 'READ')")
public ResponseEntity<?> getDataLakeMeasureName(@PathVariable("measureName")
String measureName) {
var measure =
this.dataLakeMeasureManagement.getExistingMeasureByName(measureName);
if (Objects.nonNull(measure)) {
@@ -103,6 +108,7 @@ public class DataLakeMeasureResource extends
AbstractAuthGuardedRestResource {
}
@PutMapping(path = "{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasWriteAuthority() and hasPermission(#elementId,
'WRITE')")
public ResponseEntity<?> updateDataLakeMeasure(
@PathVariable("id") String elementId,
@RequestBody DataLakeMeasure measure) {
@@ -118,6 +124,7 @@ public class DataLakeMeasureResource extends
AbstractAuthGuardedRestResource {
}
@DeleteMapping(path = "{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasWriteAuthority() and hasPermission(#elementId,
'READ')")
public ResponseEntity<?> deleteDataLakeMeasure(@PathVariable("id") String
elementId) {
try {
this.dataLakeMeasureManagement.deleteMeasurement(elementId);
@@ -126,4 +133,5 @@ public class DataLakeMeasureResource extends
AbstractAuthGuardedRestResource {
return badRequest(e.getMessage());
}
}
+
}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
index 84713d2889..5332a92bbb 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
@@ -20,7 +20,6 @@ package org.apache.streampipes.rest.impl.datalake;
import org.apache.streampipes.commons.exceptions.SpRuntimeException;
import org.apache.streampipes.dataexplorer.api.IDataExplorerQueryManagement;
-import org.apache.streampipes.dataexplorer.api.IDataExplorerSchemaManagement;
import org.apache.streampipes.dataexplorer.export.OutputFormat;
import org.apache.streampipes.dataexplorer.management.DataExplorerDispatcher;
import org.apache.streampipes.export.DataLakeExportManager;
@@ -31,7 +30,6 @@ import org.apache.streampipes.model.datalake.SpQueryResult;
import org.apache.streampipes.model.datalake.param.ProvidedRestQueryParams;
import org.apache.streampipes.model.message.Notifications;
import org.apache.streampipes.model.monitoring.SpLogMessage;
-import org.apache.streampipes.rest.core.base.impl.AbstractRestResource;
import org.apache.streampipes.rest.security.AuthConstants;
import org.apache.streampipes.rest.shared.exception.SpMessageException;
@@ -48,6 +46,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -89,30 +88,26 @@ import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryPara
@RestController
@RequestMapping("/api/v4/datalake")
-public class DataLakeResource extends AbstractRestResource {
+public class DataLakeResource extends AbstractDataLakeResource {
private static final Logger LOG =
LoggerFactory.getLogger(DataLakeResource.class);
private final IDataExplorerQueryManagement dataExplorerQueryManagement;
- private final IDataExplorerSchemaManagement dataExplorerSchemaManagement;
private static DataLakeExportManager dataLakeExportManager = new
DataLakeExportManager();
public DataLakeResource() {
- this.dataExplorerSchemaManagement = new DataExplorerDispatcher()
- .getDataExplorerManager()
- .getSchemaManagement();
+ super();
this.dataExplorerQueryManagement = new DataExplorerDispatcher()
.getDataExplorerManager()
- .getQueryManagement(this.dataExplorerSchemaManagement);
+ .getQueryManagement(this.dataLakeMeasureManagement);
}
public DataLakeResource(IDataExplorerQueryManagement
dataExplorerQueryManagement) {
+ super();
this.dataExplorerQueryManagement = dataExplorerQueryManagement;
- this.dataExplorerSchemaManagement = new DataExplorerDispatcher()
- .getDataExplorerManager()
- .getSchemaManagement();
}
@DeleteMapping(path = "/measurements/{measurementID}")
+ @PreAuthorize("this.hasWriteAuthority() and
this.checkPermissionByName(#measurementID, 'WRITE')")
@Operation(summary = "Remove data from a single measurement series with
given id", tags = {
"Data Lake" }, responses = {
@ApiResponse(responseCode = "200", description = "Data from
measurement series successfully removed"),
@@ -133,6 +128,7 @@ public class DataLakeResource extends AbstractRestResource {
}
@DeleteMapping(path = "/measurements/{measurementID}/drop")
+ @PreAuthorize("this.hasWriteAuthority() and
this.checkPermissionByName(#measurementID, 'WRITE')")
@Operation(summary = "Drop a single measurement series with given id from
Data Lake and "
+ "remove related event property", tags = {
"Data Lake" }, responses = {
@@ -144,7 +140,7 @@ public class DataLakeResource extends AbstractRestResource {
boolean isSuccessDataLake =
this.dataExplorerQueryManagement.deleteData(measurementID);
if (isSuccessDataLake) {
- boolean isSuccessEventProperty =
this.dataExplorerSchemaManagement.deleteMeasurementByName(measurementID);
+ boolean isSuccessEventProperty =
this.dataLakeMeasureManagement.deleteMeasurementByName(measurementID);
if (isSuccessEventProperty) {
return ok();
} else {
@@ -158,16 +154,19 @@ public class DataLakeResource extends
AbstractRestResource {
.body("Measurement series with given id not found.");
}
}
-
+
@GetMapping(path = "/measurements", produces =
MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Get a list of all measurement series", tags = { "Data
Lake" }, responses = {
@ApiResponse(responseCode = "200", description = "array of stored
measurement series", content = @Content(array = @ArraySchema(schema =
@Schema(implementation = DataLakeMeasure.class)))) })
- public ResponseEntity<List<DataLakeMeasure>> getAll() {
- List<DataLakeMeasure> allMeasurements =
this.dataExplorerSchemaManagement.getAllMeasurements();
- return ok(allMeasurements);
+ @PreAuthorize("this.hasReadAuthority()")
+ @PostFilter("hasPermission(filterObject.elementId, 'READ')")
+ public List<DataLakeMeasure> getAll() {
+ List<DataLakeMeasure> allMeasurements =
this.dataLakeMeasureManagement.getAllMeasurements();
+ return allMeasurements;
}
@GetMapping(path = "/measurements/{measurementId}/tags", produces =
MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasReadAuthority() and
this.checkPermissionByName(#measurementId, 'READ')")
public ResponseEntity<Map<String, Object>>
getTagValues(@PathVariable("measurementId") String measurementId,
@RequestParam("fields") String fields) {
Map<String, Object> tagValues =
dataExplorerQueryManagement.getTagValues(measurementId, fields);
@@ -175,6 +174,7 @@ public class DataLakeResource extends AbstractRestResource {
}
@GetMapping(path = "/measurements/{measurementID}", produces =
MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasReadAuthority() and
this.checkPermissionByName(#measurementID, 'READ')")
@Operation(summary = "Get data from a single measurement series by a given
id", tags = { "Data Lake" }, responses = {
@ApiResponse(responseCode = "400", description = "Measurement series
with given id and requested query specification not found"),
@ApiResponse(responseCode = "200", description = "requested data",
content = @Content(schema = @Schema(implementation = DataSeries.class))) })
@@ -215,6 +215,7 @@ public class DataLakeResource extends AbstractRestResource {
@PostMapping(path = "/query", produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<SpQueryResult>> getData(@RequestBody
List<Map<String, String>> queryParams) {
+ //TODO
var results = queryParams
.stream()
.map(qp -> new ProvidedRestQueryParams(qp.get("measureName"), qp))
@@ -225,6 +226,7 @@ public class DataLakeResource extends AbstractRestResource {
}
@GetMapping(path = "/measurements/{measurementID}/download", produces =
MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ @PreAuthorize("this.hasReadAuthority() and
this.checkPermissionByName(#measurementID, 'READ')")
@Operation(summary = "Download data from a single measurement series by a
given id", tags = {
"Data Lake" }, responses = {
@ApiResponse(responseCode = "400", description = "Measurement series
with given id and requested query specification not found"),
@@ -279,6 +281,7 @@ public class DataLakeResource extends AbstractRestResource {
}
@PostMapping(path = "/measurements/{measurementID}", produces =
MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
+ @PreAuthorize("this.hasWriteAuthority()")
@Operation(summary = "Store a measurement series to a data lake with the
given id", tags = {
"Data Lake" }, responses = {
@ApiResponse(responseCode = "400", description = "Can't store the
given data to this data lake"),
@@ -298,6 +301,7 @@ public class DataLakeResource extends AbstractRestResource {
}
@DeleteMapping(path = "/measurements")
+ @PreAuthorize(AuthConstants.IS_ADMIN_ROLE)
@Operation(summary = "Remove all stored measurement series from Data Lake",
tags = { "Data Lake" }, responses = {
@ApiResponse(responseCode = "200", description = "All measurement series
successfully removed") })
public ResponseEntity<?> removeAll() {
@@ -317,10 +321,10 @@ public class DataLakeResource extends
AbstractRestResource {
public ResponseEntity<?> setDataLakeRetention(
@PathVariable String elementId,
@RequestBody RetentionTimeConfig retention) {
- var measure = this.dataExplorerSchemaManagement.getById(elementId);
+ var measure = this.dataLakeMeasureManagement.getById(elementId);
measure.setRetentionTime(retention);
try {
- this.dataExplorerSchemaManagement.updateMeasurement(measure);
+ this.dataLakeMeasureManagement.updateMeasurement(measure);
} catch (IllegalArgumentException e) {
return badRequest(e.getMessage());
}
@@ -329,11 +333,12 @@ public class DataLakeResource extends
AbstractRestResource {
}
@DeleteMapping(path = "/{elementId}/cleanup")
+ @PreAuthorize(AuthConstants.IS_ADMIN_ROLE)
public ResponseEntity<?> deleteDataLakeRetention(@PathVariable String
elementId) {
- var measure = this.dataExplorerSchemaManagement.getById(elementId);
+ var measure = this.dataLakeMeasureManagement.getById(elementId);
measure.deleteRetentionTime();
try {
- this.dataExplorerSchemaManagement.updateMeasurement(measure);
+ this.dataLakeMeasureManagement.updateMeasurement(measure);
} catch (IllegalArgumentException e) {
return badRequest(e.getMessage());
}
@@ -348,7 +353,7 @@ public class DataLakeResource extends AbstractRestResource {
@PathVariable String elementId) {
try {
- var measure = this.dataExplorerSchemaManagement.getById(elementId);
+ var measure = this.dataLakeMeasureManagement.getById(elementId);
dataLakeExportManager.cleanupSingleMeasurement(measure);
return ok();
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/PersistedDataStreamResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/PersistedDataStreamResource.java
index 72c0e9cf2c..1bdb55d4fa 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/PersistedDataStreamResource.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/PersistedDataStreamResource.java
@@ -44,8 +44,8 @@ public class PersistedDataStreamResource extends
AbstractPipelineExtractionResou
private static final String MeasureFieldInternalName = "db_measurement";
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
- @PreAuthorize(AuthConstants.HAS_READ_DATA_EXPLORER_PRIVILEGE)
- @PostFilter("hasPermission(filterObject.pipelineId, 'READ')")
+ @PreAuthorize(AuthConstants.HAS_READ_DATASET_PRIVILEGE)
+ @PostFilter("hasPermission(filterObject.pipelineId, 'READ') and
hasPermission(filterObject.measureName, 'READ')")
public List<DataLakeMeasure> getPersistedDataStreams() {
return extract(new ArrayList<>(), DataLakeAppId);
}
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/AuthConstants.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/AuthConstants.java
index d3e19c8d56..159c4b83f4 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/AuthConstants.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/security/AuthConstants.java
@@ -20,6 +20,7 @@ package org.apache.streampipes.rest.security;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_READ_ADAPTER_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_READ_ASSETS_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_READ_DASHBOARD_VALUE;
+import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_READ_DATA_EXPLORER_VIEW_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_READ_FILES_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE;
@@ -28,6 +29,7 @@ import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constant
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_WRITE_ADAPTER_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_WRITE_ASSETS_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_WRITE_DASHBOARD_VALUE;
+import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_WRITE_DATASET_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_WRITE_FILES_VALUE;
import static
org.apache.streampipes.model.client.user.DefaultPrivilege.Constants.PRIVILEGE_WRITE_GENERIC_STORAGE_VALUE;
@@ -64,6 +66,11 @@ public class AuthConstants {
public static final String HAS_WRITE_PIPELINE_PRIVILEGE =
BS + IS_ADMIN_ROLE + OR + HAS_ANY_AUTHORITY +
PRIVILEGE_WRITE_PIPELINE_VALUE + Q + BE2;
+ public static final String HAS_READ_DATASET_PRIVILEGE =
+ BS + IS_ADMIN_ROLE + OR + HAS_ANY_AUTHORITY +
PRIVILEGE_READ_DATASET_VALUE + Q + BE2;
+ public static final String HAS_WRITE_DATASET_PRIVILEGE =
+ BS + IS_ADMIN_ROLE + OR + HAS_ANY_AUTHORITY +
PRIVILEGE_WRITE_DATASET_VALUE + Q + BE2;
+
public static final String HAS_READ_PIPELINE_ELEMENT_PRIVILEGE =
BS + IS_ADMIN_ROLE + OR + HAS_ANY_AUTHORITY +
PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE + Q + BE2;
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/filter/TokenAuthenticationFilter.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/filter/TokenAuthenticationFilter.java
index 787a5afa92..84018b0b34 100644
---
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/filter/TokenAuthenticationFilter.java
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/filter/TokenAuthenticationFilter.java
@@ -19,6 +19,7 @@
package org.apache.streampipes.service.core.filter;
import org.apache.streampipes.commons.constants.HttpConstants;
+import org.apache.streampipes.model.client.user.DefaultRole;
import org.apache.streampipes.model.client.user.Principal;
import org.apache.streampipes.model.client.user.ServiceAccount;
import org.apache.streampipes.model.client.user.UserAccount;
@@ -54,6 +55,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.List;
+import java.util.Objects;
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@@ -137,8 +139,14 @@ public class TokenAuthenticationFilter extends
OncePerRequestFilter {
private void applySuccessfulAuth(HttpServletRequest request,
String username) {
Principal user = userStorage.getUser(username);
- PrincipalUserDetails<?> userDetails = user instanceof UserAccount ? new
UserAccountDetails((UserAccount) user) :
- new ServiceAccountDetails((ServiceAccount) user);
+ PrincipalUserDetails<?> userDetails = makeDetails(user);
+ var onBehalfOfHeader = request.getHeader(HttpConstants.X_ON_BEHALF_OF);
+ if (isAdminUser(userDetails) && onBehalfOfHeader != null) {
+ var onBehalfOf = userStorage.getUserById(onBehalfOfHeader);
+ if (onBehalfOf != null) {
+ userDetails = makeDetails(onBehalfOf);
+ }
+ }
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new
WebAuthenticationDetailsSource().buildDetails(request));
@@ -146,6 +154,17 @@ public class TokenAuthenticationFilter extends
OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
+ private PrincipalUserDetails<?> makeDetails(Principal user) {
+ return user instanceof UserAccount ? new UserAccountDetails((UserAccount)
user) :
+ new ServiceAccountDetails((ServiceAccount) user);
+ }
+
+ private boolean isAdminUser(PrincipalUserDetails<?> userDetails) {
+ return userDetails.getAuthorities().stream()
+ .anyMatch(a ->
+ Objects.equals(a.getAuthority(),
DefaultRole.Constants.ROLE_ADMIN_VALUE)
+ );
+ }
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(HttpConstants.AUTHORIZATION);
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
index 1df07fd3df..fecaedcfb0 100644
---
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java
@@ -34,8 +34,10 @@ import
org.apache.streampipes.service.core.migrations.v099.AddAssetManagementVie
import
org.apache.streampipes.service.core.migrations.v099.AddScriptTemplateViewMigration;
import
org.apache.streampipes.service.core.migrations.v099.ComputeCertificateThumbprintMigration;
import
org.apache.streampipes.service.core.migrations.v099.CreateAssetPermissionMigration;
+import
org.apache.streampipes.service.core.migrations.v099.CreateDatasetPermissionMigration;
import
org.apache.streampipes.service.core.migrations.v099.ModifyAssetLinkIconMigration;
import
org.apache.streampipes.service.core.migrations.v099.MoveAssetContentMigration;
+import
org.apache.streampipes.service.core.migrations.v099.RemoveDuplicatedAssetPermissions;
import
org.apache.streampipes.service.core.migrations.v099.RemoveObsoletePrivilegesMigration;
import
org.apache.streampipes.service.core.migrations.v099.UniqueDashboardIdMigration;
import
org.apache.streampipes.service.core.migrations.v099.connect.MigrateAdaptersToUseScript;
@@ -73,12 +75,14 @@ public class AvailableMigrations {
new AddAssetManagementViewMigration(),
new MoveAssetContentMigration(),
new CreateAssetPermissionMigration(),
+ new CreateDatasetPermissionMigration(),
new RemoveObsoletePrivilegesMigration(),
new UniqueDashboardIdMigration(),
new AddScriptTemplateViewMigration(),
new ComputeCertificateThumbprintMigration(),
new MigrateAdaptersToUseScript(),
- new ModifyAssetLinkIconMigration()
+ new ModifyAssetLinkIconMigration(),
+ new RemoveDuplicatedAssetPermissions()
);
}
}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/CreateAssetPermissionMigration.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/CreateAssetPermissionMigration.java
index 7fa6ca071f..546de9ac73 100644
---
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/CreateAssetPermissionMigration.java
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/CreateAssetPermissionMigration.java
@@ -26,7 +26,6 @@ import org.apache.streampipes.storage.api.IPermissionStorage;
import org.apache.streampipes.storage.management.StorageDispatcher;
import java.io.IOException;
-import java.util.List;
public class CreateAssetPermissionMigration implements Migration {
@@ -49,7 +48,7 @@ public class CreateAssetPermissionMigration implements
Migration {
@Override
public void executeMigration() throws IOException {
assetStorage.findAll().forEach(assetModel -> {
- var existingPermission =
permissionStorage.getObjectPermissions(List.of(assetModel.getElementId()));
+ var existingPermission =
permissionStorage.getUserPermissionsForObject(assetModel.getElementId());
if (existingPermission.isEmpty()) {
permissionResourceManager.createDefault(
assetModel.getElementId(),
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/CreateDatasetPermissionMigration.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/CreateDatasetPermissionMigration.java
new file mode 100644
index 0000000000..1155f56083
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/CreateDatasetPermissionMigration.java
@@ -0,0 +1,118 @@
+/*
+ * 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.service.core.migrations.v099;
+
+import org.apache.streampipes.model.datalake.DataLakeMeasure;
+import org.apache.streampipes.model.graph.DataSinkInvocation;
+import org.apache.streampipes.model.pipeline.Pipeline;
+import org.apache.streampipes.model.staticproperty.FreeTextStaticProperty;
+import org.apache.streampipes.resource.management.PermissionResourceManager;
+import org.apache.streampipes.service.core.migrations.Migration;
+import org.apache.streampipes.storage.api.CRUDStorage;
+import org.apache.streampipes.storage.api.IPermissionStorage;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Optional;
+
+public class CreateDatasetPermissionMigration implements Migration {
+
+ private final CRUDStorage<DataLakeMeasure> dataLakeStorage;
+ private final CRUDStorage<Pipeline> pipelineStorage;
+ private final IPermissionStorage permissionStorage;
+ private final PermissionResourceManager permissionResourceManager;
+
+ private static final String DATALAKE_APP_ID =
+ "org.apache.streampipes.sinks.internal.jvm.datalake";
+
+ private static final String DB_MEASUREMENT = "db_measurement";
+
+
+ public CreateDatasetPermissionMigration() {
+ this.dataLakeStorage =
StorageDispatcher.INSTANCE.getNoSqlStore().getDataLakeStorage();
+ this.pipelineStorage =
StorageDispatcher.INSTANCE.getNoSqlStore().getPipelineStorageAPI();
+ this.permissionStorage =
StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage();
+ this.permissionResourceManager = new PermissionResourceManager();
+ }
+
+ @Override
+ public boolean shouldExecute() {
+ return true;
+ }
+
+ @Override
+ public void executeMigration() throws IOException {
+ dataLakeStorage.findAll().forEach(measure -> {
+ var existingPermission =
permissionStorage.getUserPermissionsForObject(measure.getElementId());
+
+ if (existingPermission.isEmpty()) {
+
+ permissionResourceManager.createDefault(
+ measure.getElementId(),
+ DataLakeMeasure.class,
+ findAssociatedPipelineOwner(measure),
+ true
+ );
+ }
+ });
+ }
+
+ private String findAssociatedPipelineOwner(DataLakeMeasure measure) {
+ String measureName = measure.getMeasureName();
+
+ return pipelineStorage.findAll().stream()
+ .filter(pipeline -> pipeline.getActions() != null)
+ .filter(pipeline ->
+ pipeline.getActions().stream()
+ .filter(Objects::nonNull)
+ .filter(action -> DATALAKE_APP_ID.equals(action.getAppId()))
+ .map(this::extractMeasurement)
+ .flatMap(Optional::stream)
+ .anyMatch(measureName::equals)
+ )
+ .map(pipeline -> getPipelineOwner(pipeline.getPipelineId()))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
+ }
+
+ private String getPipelineOwner(String pipelineId) {
+ var permission = permissionStorage.getUserPermissionsForObject(pipelineId);
+ if (!permission.isEmpty()) {
+ return permission.get(0).getOwnerSid();
+ }
+ return null;
+ }
+
+ private Optional<String> extractMeasurement(DataSinkInvocation datasink) {
+ return datasink.getStaticProperties().stream()
+ .filter(sp -> DB_MEASUREMENT.equals(sp.getInternalName()))
+ .filter(FreeTextStaticProperty.class::isInstance)
+ .map(FreeTextStaticProperty.class::cast)
+ .map(FreeTextStaticProperty::getValue)
+ .filter(value -> !value.isBlank())
+ .findFirst();
+ }
+
+ @Override
+ public String getDescription() {
+ return "Create default permissions for datasets";
+ }
+}
diff --git
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/RemoveDuplicatedAssetPermissions.java
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/RemoveDuplicatedAssetPermissions.java
new file mode 100644
index 0000000000..b2918b23a5
--- /dev/null
+++
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v099/RemoveDuplicatedAssetPermissions.java
@@ -0,0 +1,106 @@
+/*
+ * 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.service.core.migrations.v099;
+
+import org.apache.streampipes.model.assets.SpAssetModel;
+import org.apache.streampipes.model.client.user.Permission;
+import org.apache.streampipes.service.core.migrations.Migration;
+import org.apache.streampipes.storage.api.CRUDStorage;
+import org.apache.streampipes.storage.api.IPermissionStorage;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class RemoveDuplicatedAssetPermissions implements Migration {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(RemoveDuplicatedAssetPermissions.class);
+
+ private final IPermissionStorage permissionStorage =
+ StorageDispatcher.INSTANCE.getNoSqlStore().getPermissionStorage();
+
+ private final CRUDStorage<SpAssetModel> assetStorage =
+ StorageDispatcher.INSTANCE.getNoSqlStore().getAssetStorage();
+
+ @Override
+ public boolean shouldExecute() {
+ return true;
+ }
+
+ @Override
+ public void executeMigration() throws IOException {
+ AtomicInteger deletedPermissions = new AtomicInteger();
+ assetStorage.findAll().forEach(asset -> {
+ var permissions =
permissionStorage.getUserPermissionsForObject(asset.getElementId());
+ if (permissions.size() > 1) {
+
+ // "Bad" permissions = no owner, empty grantedAuthorities, and public
+ var badPermissions = permissions.stream()
+ .filter(this::isAccidentalPermission)
+ .toList();
+
+ // "Good" permissions = everything else
+ var goodPermissions = permissions.stream()
+ .filter(p -> !isAccidentalPermission(p))
+ .toList();
+
+ // Choose what to keep:
+ // - keep first GOOD if exists
+ // - otherwise keep first BAD (meaning all are bad)
+ var keep = !goodPermissions.isEmpty()
+ ? goodPermissions.get(0)
+ : badPermissions.get(0);
+
+ // Delete everything else
+ for (var p : permissions) {
+ if (!(keep.getElementId().equals(p.getElementId()))) {
+ permissionStorage.deleteElementById(p.getElementId());
+ deletedPermissions.getAndIncrement();
+ }
+ }
+ }
+ });
+ LOG.info("Deleted {} permissions", deletedPermissions.get());
+ }
+
+ private boolean isAccidentalPermission(Permission p) {
+ if (p == null) {
+ return false;
+ }
+
+ var owner = p.getOwnerSid();
+ boolean noOwner = (owner == null) || owner.isBlank();
+
+ var granted = p.getGrantedAuthorities();
+ boolean emptyAuthorities = (granted == null) || granted.isEmpty();
+
+ boolean isPublic = p.isPublicElement();
+
+ return noOwner && emptyAuthorities && isPublic;
+ }
+
+
+ @Override
+ public String getDescription() {
+ return "Remove duplicated asset permissions";
+ }
+}
diff --git
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/PrivilegeManager.java
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/PrivilegeManager.java
index dd850d5de4..4e76d959d9 100644
---
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/PrivilegeManager.java
+++
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/PrivilegeManager.java
@@ -37,6 +37,7 @@ public class PrivilegeManager {
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_READ_NOTIFICATIONS_VALUE),
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE),
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE),
+
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE),
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_ADAPTER_VALUE),
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_ASSETS_VALUE),
@@ -45,7 +46,8 @@ public class PrivilegeManager {
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_FILES_VALUE),
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_GENERIC_STORAGE_VALUE),
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_PIPELINE_VALUE),
-
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_PIPELINE_ELEMENT_VALUE)
+
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_PIPELINE_ELEMENT_VALUE),
+
Privilege.create(DefaultPrivilege.Constants.PRIVILEGE_WRITE_DATASET_VALUE)
diff --git
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java
index a30f3a682a..5ca24b5223 100644
---
a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java
+++
b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authorization/RoleManager.java
@@ -44,12 +44,14 @@ public class RoleManager {
DefaultPrivilege.Constants.PRIVILEGE_READ_DASHBOARD_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_WRITE_DASHBOARD_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_READ_DATA_EXPLORER_VIEW_VALUE,
- DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE
+ DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE,
+ DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE
)),
Role.createDefaultRole(DefaultRole.Constants.ROLE_DASHBOARD_USER_VALUE,
"Dashboard User", List.of(
DefaultPrivilege.Constants.PRIVILEGE_READ_DASHBOARD_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_READ_DATA_EXPLORER_VIEW_VALUE,
- DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE
+ DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE,
+ DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE
)),
Role.createDefaultRole(DefaultRole.Constants.ROLE_PIPELINE_ADMIN_VALUE,
"Pipeline Admin", List.of(
DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE,
@@ -57,13 +59,16 @@ public class RoleManager {
DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_READ_FILES_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_WRITE_FILES_VALUE,
- DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE
+ DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE,
+ DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE,
+ DefaultPrivilege.Constants.PRIVILEGE_WRITE_DATASET_VALUE
)),
Role.createDefaultRole(DefaultRole.Constants.ROLE_PIPELINE_USER_VALUE,
"Pipeline User", List.of(
DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_READ_PIPELINE_ELEMENT_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_READ_FILES_VALUE,
- DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE
+ DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE,
+ DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE
)),
Role.createDefaultRole(DefaultRole.Constants.ROLE_ASSET_ADMIN_VALUE,
"Asset Admin", List.of(
DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE,
@@ -82,11 +87,13 @@ public class RoleManager {
Role.createDefaultRole(DefaultRole.Constants.ROLE_DATA_EXPLORER_ADMIN_VALUE,
"Data Explorer Admin", List.of(
DefaultPrivilege.Constants.PRIVILEGE_READ_DATA_EXPLORER_VIEW_VALUE,
DefaultPrivilege.Constants.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW_VALUE,
- DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE
+ DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE,
+ DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE
)),
Role.createDefaultRole(DefaultRole.Constants.ROLE_DATA_EXPLORER_USER_VALUE,
"Data Explorer User", List.of(
DefaultPrivilege.Constants.PRIVILEGE_READ_DATA_EXPLORER_VIEW_VALUE,
- DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE
+ DefaultPrivilege.Constants.PRIVILEGE_READ_GENERIC_STORAGE_VALUE,
+ DefaultPrivilege.Constants.PRIVILEGE_READ_DATASET_VALUE
)),
Role.createDefaultRole(DefaultRole.Constants.ROLE_CONNECT_ADMIN_VALUE,
"Connect Admin", List.of(
DefaultPrivilege.Constants.PRIVILEGE_WRITE_ADAPTER_VALUE,
diff --git a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
index 4af3b4fa53..f644453904 100644
--- a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
+++ b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
@@ -33,6 +33,10 @@ export class DataExplorerBtns {
return cy.dataCy('save-dashboard-btn');
}
+ public static discardDashboard() {
+ return cy.dataCy('discard-dashboard-btn');
+ }
+
public static saveChartsToAssetBtn() {
return cy
.dataCy('add-to-Asset-data-view-btn', { timeout: 10000 })
@@ -106,6 +110,10 @@ export class DataExplorerBtns {
return cy.dataCy('edit-' + widgetName);
}
+ public static viewWidget(widgetName: string) {
+ return cy.dataCy('show-data-view-' + widgetName);
+ }
+
public static moreOptionsBtn(widgetName) {
return cy.dataCy('more-options-' + widgetName);
}
diff --git a/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
b/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
index d174295010..e918092423 100644
--- a/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
+++ b/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
@@ -442,6 +442,13 @@ export class DataExplorerUtils {
.click();
}
+ public static assertSelectDataSet(dataSet: string) {
+ cy.dataCy('data-explorer-select-data-set')
+ .click()
+ .get('mat-option')
+ .should('contain.text', dataSet);
+ }
+
/**
* Checks if in the widget configuration the filters are set or not
* @param amountOfFilter the amount of filters that should be set. 0 if no
filter should be visible
diff --git a/ui/cypress/support/utils/dataset/DatasetBtns.ts
b/ui/cypress/support/utils/dataset/DatasetBtns.ts
new file mode 100644
index 0000000000..36985d2541
--- /dev/null
+++ b/ui/cypress/support/utils/dataset/DatasetBtns.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ *
+ */
+
+export class DatasetBtns {
+ public static datasetTable() {
+ return cy.dataCy('datalake-settings', { timeout: 10000 });
+ }
+}
diff --git a/ui/cypress/support/utils/dataset/DatasetUtils.ts
b/ui/cypress/support/utils/dataset/DatasetUtils.ts
new file mode 100644
index 0000000000..fbda2e8b20
--- /dev/null
+++ b/ui/cypress/support/utils/dataset/DatasetUtils.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 { PermissionUtils } from '../user/PermissionUtils';
+import { DatasetBtns } from './DatasetBtns';
+
+export class DatasetUtils {
+ public static goToDatasets() {
+ cy.visit('#/datasets');
+ }
+
+ public static checkAmountOfDatasets(amount: number) {
+ DatasetUtils.goToDatasets();
+
+ 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 {
+ DatasetBtns.datasetTable().should('have.length', amount);
+ }
+ }
+
+ public static authorizeUserOnDataset(datasetname: string, email: string) {
+ DatasetUtils.goToDatasets();
+ PermissionUtils.authorizeUser(datasetname, email);
+ }
+}
diff --git a/ui/cypress/tests/userManagement/testUserRoleDataset.spec.ts
b/ui/cypress/tests/userManagement/testUserRoleDataset.spec.ts
new file mode 100644
index 0000000000..c855272410
--- /dev/null
+++ b/ui/cypress/tests/userManagement/testUserRoleDataset.spec.ts
@@ -0,0 +1,221 @@
+/*
+ * 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 { UserRole } from '../../../src/app/_enums/user-role.enum';
+import { UserUtils } from '../../support/utils/UserUtils';
+import { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
+import { User } from '../../support/model/User';
+import { DataExplorerUtils } from
'../../support/utils/dataExplorer/DataExplorerUtils';
+import { PermissionUtils } from '../../support/utils/user/PermissionUtils';
+import { DataExplorerBtns } from
'../../support/utils/dataExplorer/DataExplorerBtns';
+import { DatasetUtils } from '../../support/utils/dataset/DatasetUtils';
+import { GeneralUtils } from '../../support/utils/GeneralUtils';
+
+describe('Test Dataset Permissions', () => {
+ const datasetName = 'Persist simulator';
+ let datasetUser1: User;
+ let datasetAdmin1: User;
+ let datasetAdmin2: User;
+ let chartAdmin1: User;
+ let chartUser1: User;
+ let dashboardAdmin1: User;
+
+ beforeEach('Setup Test', () => {
+ cy.initStreamPipesTest();
+
+ datasetUser1 = UserUtils.createUser(
+ 'datasetUser1',
+ UserRole.ROLE_PIPELINE_USER,
+ );
+
+ datasetAdmin1 = UserUtils.createUser(
+ 'datasetAdmin1',
+ UserRole.ROLE_CONNECT_ADMIN,
+ UserRole.ROLE_PIPELINE_ADMIN,
+ );
+
+ datasetAdmin2 = UserUtils.createUser(
+ 'datasetAdmin2',
+ UserRole.ROLE_PIPELINE_ADMIN,
+ );
+
+ chartAdmin1 = UserUtils.createUser(
+ 'chartAdmin1',
+ UserRole.ROLE_DATA_EXPLORER_ADMIN,
+ );
+
+ chartUser1 = UserUtils.createUser(
+ 'chartUser1',
+ UserRole.ROLE_DATA_EXPLORER_USER,
+ );
+
+ dashboardAdmin1 = UserUtils.createUser(
+ 'dashboardAdmin1',
+ UserRole.ROLE_DASHBOARD_ADMIN,
+ );
+ });
+
+ it('Dataset is not shared with other users', () => {
+ generateDataset();
+
+ assertDatasetIsVisibleAndEditableCanChangePermissions(
+ UserUtils.adminUser,
+ );
+
+ assertDatasetIsNotVisible(datasetUser1);
+
+ UserUtils.switchUser(datasetUser1);
+
+ assertDatasetIsNotVisible(datasetAdmin2);
+ });
+
+ it('Datasets only usable in charts if permissions were configured', () => {
+ generateDataset();
+
+ UserUtils.switchUser(chartAdmin1);
+
+ assertDatasetAvailabilityInCharts(false);
+
+ authUserOnDataset('[email protected]');
+
+ UserUtils.switchUser(chartAdmin1);
+
+ assertDatasetAvailabilityInCharts(true);
+
+ DataExplorerUtils.goToDatalake();
+
+ PermissionUtils.authorizeUser(
+ 'test',
+ '[email protected]',
+ );
+
+ // I am not quite sure why this is needed, but without it the test
fails
+ cy.dataCy('confirm-delete', { timeout: 10000 }).click();
+
+ UserUtils.switchUser(chartUser1);
+
+ DataExplorerUtils.checkAmountOfCharts(1);
+
+ GeneralUtils.openMenuForRow('test');
+
+ DataExplorerBtns.viewWidget('test').click();
+
+ assertAlertBanner(true);
+
+ assertDatasetIsNotVisible(chartUser1);
+
+ authUserOnDataset('[email protected]');
+
+ UserUtils.switchUser(chartUser1);
+
+ DataExplorerUtils.checkAmountOfCharts(1);
+
+ GeneralUtils.openMenuForRow('test');
+
+ DataExplorerBtns.viewWidget('test').click();
+
+ assertAlertBanner(false);
+ });
+
+ it('Data only shown in dashboard if permissions were configured', () => {
+ generateDataset();
+
+ authUserOnDataset('[email protected]');
+
+ UserUtils.switchUser(chartAdmin1);
+
+ assertDatasetAvailabilityInCharts(true);
+
+ PermissionUtils.authorizeUser(
+ 'test',
+ '[email protected]',
+ );
+
+ UserUtils.switchUser(dashboardAdmin1);
+
+ generateDashboard('TestDB');
+
+ assertAlertBanner(true);
+
+ DataExplorerBtns.discardDashboard().click();
+
+ assertDatasetIsNotVisible(dashboardAdmin1);
+
+ authUserOnDataset('[email protected]');
+
+ UserUtils.switchUser(dashboardAdmin1);
+
+ generateDashboard('TestDB2');
+
+ assertAlertBanner(false);
+ });
+
+ function assertDatasetAvailabilityInCharts(available: boolean) {
+ DataExplorerUtils.goToDatalake();
+ DataExplorerBtns.openNewDataViewBtn().click();
+ if (!available) {
+ cy.get('sp-alert-banner').should('be.visible');
+ } else {
+ DataExplorerUtils.assertSelectDataSet('simulator');
+ DataExplorerUtils.addDataViewAndTableWidget(
+ 'test',
+ 'simulator',
+ true,
+ );
+ DataExplorerUtils.saveDataViewConfiguration();
+ }
+ }
+
+ function generateDataset() {
+ UserUtils.switchUser(datasetAdmin1);
+ ConnectUtils.addMachineDataSimulator('simulator', true);
+ }
+
+ function generateDashboard(name: string) {
+ DataExplorerUtils.goToDashboard();
+ DataExplorerUtils.createNewDashboard(name);
+ DataExplorerUtils.editDashboard(name);
+ DataExplorerUtils.addDataViewToDashboard('test', true);
+ }
+ function assertDatasetIsVisibleAndEditableCanChangePermissions(user: User)
{
+ UserUtils.switchUser(user);
+ DatasetUtils.goToDatasets();
+ DatasetUtils.checkAmountOfDatasets(1);
+ PermissionUtils.validateUserCanChangePermissions(datasetName);
+ }
+
+ function assertDatasetIsNotVisible(user: User) {
+ UserUtils.switchUser(user);
+ DatasetUtils.goToDatasets();
+ DatasetUtils.checkAmountOfDatasets(0);
+ }
+
+ function assertAlertBanner(exists: boolean) {
+ if (exists) {
+ cy.dataCy('sp-alert-banner-error').should('exist');
+ } else {
+ cy.dataCy('sp-alert-banner-error').should('not.exist');
+ }
+ }
+
+ function authUserOnDataset(email: string) {
+ UserUtils.switchUser(datasetAdmin1);
+
+ DatasetUtils.authorizeUserOnDataset('simulator', email);
+ }
+});
diff --git a/ui/deployment/modules.yml b/ui/deployment/modules.yml
index f43600590b..5eb66406be 100644
--- a/ui/deployment/modules.yml
+++ b/ui/deployment/modules.yml
@@ -37,7 +37,7 @@ spDatasets:
title: 'Datasets'
description: 'Manage datasets'
icon: 'dataset'
- privileges: '[UserRole.ROLE_ADMIN]'
+ privileges: '[UserPrivilege.PRIVILEGE_READ_DATASET,
UserPrivilege.PRIVILEGE_WRITE_DATASET]'
pageNames: 'PageName.DATASETS'
showStatusBox: false
category: 'management'
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
index cc55f6fce2..23efa1c963 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
@@ -16,12 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
+
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2026-01-13
08:54:20.
+// Generated using typescript-generator version 3.2.1263 on 2026-01-15
16:03:19.
-import { Storable } from './platform-services';
+import { Storable } from './streampipes-model';
export class Group implements Storable {
alternateIds: string[];
@@ -309,6 +310,8 @@ export type DefaultPrivilege =
| 'PRIVILEGE_WRITE_ADAPTER'
| 'PRIVILEGE_READ_PIPELINE_ELEMENT'
| 'PRIVILEGE_WRITE_PIPELINE_ELEMENT'
+ | 'PRIVILEGE_READ_DATASET'
+ | 'PRIVILEGE_WRITE_DATASET'
| 'PRIVILEGE_READ_DASHBOARD'
| 'PRIVILEGE_WRITE_DASHBOARD'
| 'PRIVILEGE_READ_DATA_EXPLORER_VIEW'
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index 558d5816a7..a0fced120e 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -16,10 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
+
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2026-01-13
08:54:16.
+// Generated using typescript-generator version 3.2.1263 on 2026-01-15
16:03:01.
export class NamedStreamPipesEntity implements Storable {
'@class':
diff --git
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html
index 63615e61a1..543abca116 100644
---
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-exception-message/sp-exception-message.component.html
@@ -19,6 +19,7 @@
<div fxFlex="100" fxLayout="row" class="w-100" data-cy="display-exception">
<sp-alert-banner
type="error"
+ data-cy="sp-alert-banner-error"
class="w-100"
[showDetailsButton]="showDetails"
(detailsButtonClicked)="openDetailsDialog()"
diff --git a/ui/src/app/_enums/user-privilege.enum.ts
b/ui/src/app/_enums/user-privilege.enum.ts
index acde97b208..1de793b088 100644
--- a/ui/src/app/_enums/user-privilege.enum.ts
+++ b/ui/src/app/_enums/user-privilege.enum.ts
@@ -45,4 +45,7 @@ export enum UserPrivilege {
PRIVILEGE_READ_LABELS = 'PRIVILEGE_READ_LABELS',
PRIVILEGE_WRITE_LABELS = 'PRIVILEGE_WRITE_LABELS',
+
+ PRIVILEGE_READ_DATASET = 'PRIVILEGE_READ_DATASET',
+ PRIVILEGE_WRITE_DATASET = 'PRIVILEGE_WRITE_DATASET',
}
diff --git
a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html
b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html
index 284bb1261d..fee14dfa0a 100644
---
a/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html
+++
b/ui/src/app/chart/components/chart-overview/chart-overview-table/chart-overview-table.component.html
@@ -95,7 +95,17 @@
</ng-container>
<ng-template spTableActions let-element>
- <button mat-menu-item (click)="openDataView(element, false)">
+ <button
+ mat-menu-item
+ [attr.data-cy]="
+ 'show-data-view-' +
+ element.baseAppearanceConfig.widgetTitle.replaceAll(
+ ' ',
+ ''
+ )
+ "
+ (click)="openDataView(element, false)"
+ >
<mat-icon>visibility</mat-icon>
<span>{{ 'Show' | translate }}</span>
</button>
diff --git
a/ui/src/app/configuration/files/file-overview/file-overview.component.html
b/ui/src/app/configuration/files/file-overview/file-overview.component.html
index d0b91930ff..a7fa9267f5 100644
--- a/ui/src/app/configuration/files/file-overview/file-overview.component.html
+++ b/ui/src/app/configuration/files/file-overview/file-overview.component.html
@@ -20,25 +20,26 @@
<ng-container matColumnDef="filename">
<th mat-header-cell *matHeaderCellDef>{{ 'Filename' | translate }}</th>
<td mat-cell *matCellDef="let fileMetadata">
- <h4>{{ fileMetadata.filename }}</h4>
+ <span>{{ fileMetadata.filename }}</span>
</td>
</ng-container>
<ng-container matColumnDef="filetype">
<th mat-header-cell *matHeaderCellDef>{{ 'Filetype' | translate }}</th>
<td mat-cell *matCellDef="let fileMetadata">
- <span
- class="filetype-container"
- [style.background-color]="getFileColor(fileMetadata.filetype)"
- >{{ fileMetadata.filetype }}</span
+ <sp-label
+ [color]="getFileColor(fileMetadata.filetype)"
+ size="medium"
+ [labelText]="fileMetadata.filetype"
>
+ </sp-label>
</td>
</ng-container>
<ng-container matColumnDef="uploaded">
<th mat-header-cell *matHeaderCellDef>{{ 'Uploaded' | translate }}</th>
<td mat-cell *matCellDef="let fileMetadata">
- <h5>
+ <span>
{{ fileMetadata.createdAt | date: 'dd.MM.yyyy HH:mm' }}
- </h5>
+ </span>
</td>
</ng-container>
diff --git
a/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html
b/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html
index 0d85549398..ff335b85ae 100644
---
a/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html
+++
b/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html
@@ -39,6 +39,7 @@
[matTooltip]="'Discard' | translate"
class="mat-basic mr-10 edit-menu-btn"
(click)="discardDashboardEmitter.emit()"
+ data-cy="discard-dashboard-btn"
>
<i class="material-icons">undo</i>
<span> {{ 'Discard' | translate }}</span>
diff --git
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
index a6aa18e361..c181b22821 100644
---
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
+++
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.html
@@ -163,75 +163,114 @@
{{ 'Retention' | translate }}
</th>
<td mat-cell *matCellDef="let configurationEntry">
- <button
- color="accent"
- mat-icon-button
- [matTooltip]="'Set retention rate' | translate"
- data-cy="datalake-retention-btn"
- matTooltipPosition="above"
- (click)="
- openRetentionDialog(
- configurationEntry.elementId
- )
- "
- >
- <i
- class="material-icons"
- [ngStyle]="{
- color: configurationEntry?.retention
- ? 'var(--color-success)'
- : 'var(--color-neutral',
- }"
- >history</i
+ <div fxLayout="row">
+ <span
+ fxFlex
+ fxFlexOrder="3"
+ fxLayout="row"
+ fxLayoutAlign="start center"
>
- </button>
- @if (
-
configurationEntry?.retention?.retentionExportConfig
- ?.retentionLog?.length > 0
- ) {
- <button
- color="accent"
- mat-icon-button
- [matTooltip]="
- ('Open Retention Log' | translate) +
- (configurationEntry?.retention
- ?.retentionExportConfig?.lastExport
- ? ' • ' +
- (configurationEntry.retention
- .retentionExportConfig.lastExport
- | date: 'yyyy-MM-dd HH:mm:ss')
- : '')
- "
- data-cy="datalake-retention-log-btn"
- matTooltipPosition="above"
- (click)="
- openRetentionLog(
- configurationEntry?.retention
- .retentionExportConfig.retentionLog
- )
- "
+ @if (isAdmin) {
+ <button
+ color="accent"
+ mat-icon-button
+ [matTooltip]="
+ 'Set retention rate' | translate
+ "
+ data-cy="datalake-retention-btn"
+ matTooltipPosition="above"
+ (click)="
+ openRetentionDialog(
+ configurationEntry.elementId
+ )
+ "
+ >
+ <i
+ class="material-icons"
+ [ngStyle]="{
+ color:
configurationEntry?.retention
+ ? 'var(--color-success)'
+ : 'var(--color-neutral',
+ }"
+ >history</i
+ >
+ </button>
+ } @else {
+ <p>-</p>
+ }
+ </span>
+ </div>
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="retentionlog">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Retention Log' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let configurationEntry">
+ <div fxLayout="row">
+ <span
+ fxFlex
+ fxFlexOrder="3"
+ fxLayout="row"
+ fxLayoutAlign="start center"
>
- <i
- class="material-icons"
- [ngStyle]="{
- color:
- configurationEntry?.retention
+ @if (configurationEntry?.retention) {
+ <button
+ color="accent"
+ mat-icon-button
+ [matTooltip]="
+ ('Open Retention Log' | translate)
+
+ (configurationEntry?.retention
?.retentionExportConfig
- ?.retentionLog?.length &&
- configurationEntry.retention
- .retentionExportConfig
- .retentionLog[
- configurationEntry.retention
+ ?.lastExport
+ ? ' • ' +
+ (configurationEntry.retention
+ .retentionExportConfig
+ .lastExport
+ | date
+ : 'yyyy-MM-dd
HH:mm:ss')
+ : '')
+ "
+ data-cy="datalake-retention-log-btn"
+ matTooltipPosition="above"
+ (click)="
+ openRetentionLog(
+ configurationEntry?.retention
.retentionExportConfig
- .retentionLog.length - 1
- ].status
- ? 'var(--color-success)'
- : 'var(--color-error)',
- }"
- >list_alt</i
- >
- </button>
- }
+ .retentionLog
+ )
+ "
+ >
+ <i
+ class="material-icons"
+ [ngStyle]="{
+ color:
+ configurationEntry
+ ?.retention
+ ?.retentionExportConfig
+ ?.retentionLog
+ ?.length &&
+
configurationEntry.retention
+ .retentionExportConfig
+ .retentionLog[
+ configurationEntry
+ .retention
+
.retentionExportConfig
+ .retentionLog
+ .length - 1
+ ].status
+ ? 'green'
+ : 'red',
+ }"
+ >list_alt</i
+ >
+ </button>
+ } @else {
+ <p>-</p>
+ }
+ </span>
+ </div>
</td>
</ng-container>
@@ -246,223 +285,241 @@
'Download data from index' | translate
}}</span>
</button>
- <button
- mat-menu-item
- data-cy="datalake-truncate-btn"
- (click)="cleanDatalakeIndex(element.name)"
- >
- <mat-icon>local_fire_department</mat-icon>
- <span>{{
- 'Truncate all data from index' | translate
- }}</span>
- </button>
- <button
- mat-menu-item
- data-cy="datalake-delete-btn"
- [disabled]="!element.remove"
- (click)="deleteDatalakeIndex(element.name)"
- >
- <mat-icon>delete</mat-icon>
- <span>{{
- 'Remove index from database' | translate
- }}</span>
- </button>
+ @if (writeAccess) {
+ <button
+ mat-menu-item
+ data-cy="datalake-truncate-btn"
+ (click)="cleanDatalakeIndex(element.name)"
+ >
+ <mat-icon>local_fire_department</mat-icon>
+ <span>{{
+ 'Truncate all data from index' | translate
+ }}</span>
+ </button>
+ <button
+ mat-menu-item
+ data-cy="datalake-delete-btn"
+ [disabled]="!element.remove"
+ (click)="deleteDatalakeIndex(element.name)"
+ >
+ <mat-icon>delete</mat-icon>
+ <span>{{
+ 'Remove index from database' | translate
+ }}</span>
+ </button>
+ <button
+ mat-menu-item
+ data-cy="open-manage-permissions"
+ (click)="showPermissionsDialog(element)"
+ >
+ <mat-icon>share</mat-icon>
+ <span>{{ 'Manage permissions' | translate }}</span>
+ </button>
+ }
</ng-template>
</sp-table>
</div>
-
- <div
- fxLayout="row"
- fxFlex="100"
- class="mt-10 w-100"
- fxLayoutAlign="start center"
- >
- <sp-basic-header-title-component
- [title]="'Export Providers' | translate"
- [description]="
- 'Add, Edit, and Delete export providers used for backing
up data lakes.'
- | translate
- "
- ></sp-basic-header-title-component>
- <span fxFlex></span>
+ @if (isAdmin) {
<div
- fxFlex
fxLayout="row"
- fxLayoutAlign="end center"
- fxLayoutGap="5px"
+ fxFlex="100"
+ class="mt-10 w-100"
+ fxLayoutAlign="start center"
>
- <button
- mat-flat-button
- matTooltip="{{ 'New' | translate }}"
- data-cy="new-export-providers"
- (click)="createExportProvider(null)"
+ <sp-basic-header-title-component
+ [title]="'Export Providers' | translate"
+ [description]="
+ 'Add, Edit, and Delete export providers used for
backing up data lakes.'
+ | translate
+ "
+ ></sp-basic-header-title-component>
+ <span fxFlex></span>
+ <div
+ fxFlex
+ fxLayout="row"
+ fxLayoutAlign="end center"
+ fxLayoutGap="5px"
>
- <mat-icon>add</mat-icon>
- <span>{{ 'New' | translate }}</span>
- </button>
- <button
- mat-icon-button
- matTooltip="{{ 'Refresh' | translate }}"
- data-cy="refresh-export-providers-measures"
- (click)="loadAvailableExportProvider()"
- >
- <mat-icon>refresh</mat-icon>
- </button>
+ <button
+ mat-flat-button
+ matTooltip="{{ 'New' | translate }}"
+ data-cy="new-export-providers"
+ (click)="createExportProvider(null)"
+ >
+ <mat-icon>add</mat-icon>
+ <span>{{ 'New' | translate }}</span>
+ </button>
+ <button
+ mat-icon-button
+ matTooltip="{{ 'Refresh' | translate }}"
+ data-cy="refresh-export-providers-measures"
+ (click)="loadAvailableExportProvider()"
+ >
+ <mat-icon>refresh</mat-icon>
+ </button>
+ </div>
</div>
- </div>
- <table
- fxFlex="100"
- mat-table
- data-cy="exportproviders-settings"
- [dataSource]="dataSourceExport"
- style="width: 100%"
- matSort
- >
- <ng-container matColumnDef="providertype">
- <th mat-header-cell mat-sort-header *matHeaderCellDef>
- {{ 'Provider Type' | translate }}
- </th>
- <td mat-cell *matCellDef="let configurationEntry">
- {{ configurationEntry.providerType }}
- </td>
- </ng-container>
- <ng-container matColumnDef="endpoint">
- <th mat-header-cell mat-sort-header *matHeaderCellDef>
- {{ 'Endpoint' | translate }}
- </th>
- <td mat-cell *matCellDef="let configurationEntry">
- {{ configurationEntry.endPoint }}
- </td>
- </ng-container>
+ <table
+ fxFlex="100"
+ mat-table
+ data-cy="exportproviders-settings"
+ [dataSource]="dataSourceExport"
+ style="width: 100%"
+ matSort
+ >
+ <ng-container matColumnDef="providertype">
+ <th mat-header-cell mat-sort-header *matHeaderCellDef>
+ {{ 'Provider Type' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let configurationEntry">
+ {{ configurationEntry.providerType }}
+ </td>
+ </ng-container>
- <ng-container matColumnDef="bucket">
- <th mat-header-cell mat-sort-header *matHeaderCellDef>
- {{ 'Bucket' | translate }}
- </th>
- <td mat-cell *matCellDef="let configurationEntry">
- {{ configurationEntry.bucketName }}
- </td>
- </ng-container>
+ <ng-container matColumnDef="endpoint">
+ <th mat-header-cell mat-sort-header *matHeaderCellDef>
+ {{ 'Endpoint' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let configurationEntry">
+ {{ configurationEntry.endPoint }}
+ </td>
+ </ng-container>
- <ng-container matColumnDef="editExportProvider">
- <th mat-header-cell *matHeaderCellDef>
- {{ 'Edit' | translate }}
- </th>
- <td mat-cell *matCellDef="let configurationEntry">
- <div *ngIf="configurationEntry.providerId !== 'FOLDER'">
- <div fxLayout="row">
- <span
- fxFlex
- fxFlexOrder="3"
- fxLayout="row"
- fxLayoutAlign="start center"
- >
- <button
- color="accent"
- mat-icon-button
- matTooltip="{{
- 'Edit Export Provider' | translate
- }}"
- matTooltipPosition="above"
- data-cy="exportProvider-edit-btn"
- (click)="
-
createExportProvider(configurationEntry)
- "
+ <ng-container matColumnDef="bucket">
+ <th mat-header-cell mat-sort-header *matHeaderCellDef>
+ {{ 'Bucket' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let configurationEntry">
+ {{ configurationEntry.bucketName }}
+ </td>
+ </ng-container>
+
+ <ng-container matColumnDef="editExportProvider">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Edit' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let configurationEntry">
+ <div *ngIf="configurationEntry.providerId !==
'FOLDER'">
+ <div fxLayout="row">
+ <span
+ fxFlex
+ fxFlexOrder="3"
+ fxLayout="row"
+ fxLayoutAlign="start center"
>
- <i class="material-icons">edit</i>
- </button>
- </span>
+ <button
+ color="accent"
+ mat-icon-button
+ matTooltip="{{
+ 'Edit Export Provider' | translate
+ }}"
+ matTooltipPosition="above"
+ data-cy="exportProvider-edit-btn"
+ (click)="
+ createExportProvider(
+ configurationEntry
+ )
+ "
+ >
+ <i class="material-icons">edit</i>
+ </button>
+ </span>
+ </div>
</div>
- </div>
- </td>
- </ng-container>
+ </td>
+ </ng-container>
- <ng-container matColumnDef="delete">
- <th mat-header-cell *matHeaderCellDef>
- {{ 'Remove' | translate }}
- </th>
- <td mat-cell *matCellDef="let configurationEntry">
- <div *ngIf="configurationEntry.providerId !== 'FOLDER'">
- <div fxLayout="row">
- <span
- fxFlex
- fxFlexOrder="3"
- fxLayout="row"
- fxLayoutAlign="start center"
- >
- <button
- color="accent"
- mat-icon-button
- matTooltip="{{
- 'Remove export provider configuration'
- | translate
- }}"
- data-cy="exportProvider-delete-btn"
- matTooltipPosition="above"
- (click)="
- deleteExportProvider(
- configurationEntry.providerId
- )
- "
+ <ng-container matColumnDef="delete">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Remove' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let configurationEntry">
+ <div *ngIf="configurationEntry.providerId !==
'FOLDER'">
+ <div fxLayout="row">
+ <span
+ fxFlex
+ fxFlexOrder="3"
+ fxLayout="row"
+ fxLayoutAlign="start center"
>
- <i class="material-icons">delete</i>
- </button>
- </span>
+ <button
+ color="accent"
+ mat-icon-button
+ matTooltip="{{
+ 'Remove export provider
configuration'
+ | translate
+ }}"
+ data-cy="exportProvider-delete-btn"
+ matTooltipPosition="above"
+ (click)="
+ deleteExportProvider(
+ configurationEntry.providerId
+ )
+ "
+ >
+ <i class="material-icons">delete</i>
+ </button>
+ </span>
+ </div>
</div>
- </div>
- </td>
- </ng-container>
+ </td>
+ </ng-container>
- <ng-container matColumnDef="test">
- <th mat-header-cell *matHeaderCellDef>
- {{ 'Test' | translate }}
- </th>
- <td mat-cell *matCellDef="let configurationEntry">
- <div>
- <div fxLayout="row">
- <span
- fxFlex
- fxFlexOrder="3"
- fxLayout="row"
- fxLayoutAlign="start center"
- >
- <button
- color="accent"
- mat-icon-button
- matTooltip="{{
- 'Test export provider configuration'
- | translate
- }}"
- data-cy="exportProvider-test-btn"
- matTooltipPosition="above"
- (click)="
- testExportProvider(
- configurationEntry.providerId
- )
- "
+ <ng-container matColumnDef="test">
+ <th mat-header-cell *matHeaderCellDef>
+ {{ 'Test' | translate }}
+ </th>
+ <td mat-cell *matCellDef="let configurationEntry">
+ <div>
+ <div fxLayout="row">
+ <span
+ fxFlex
+ fxFlexOrder="3"
+ fxLayout="row"
+ fxLayoutAlign="start center"
>
- <i class="material-icons">bug_report</i>
- </button>
- </span>
+ <button
+ color="accent"
+ mat-icon-button
+ matTooltip="{{
+ 'Test export provider
configuration'
+ | translate
+ }}"
+ data-cy="exportProvider-test-btn"
+ matTooltipPosition="above"
+ (click)="
+ testExportProvider(
+ configurationEntry.providerId
+ )
+ "
+ >
+ <i
class="material-icons">bug_report</i>
+ </button>
+ </span>
+ </div>
</div>
- </div>
- </td>
- </ng-container>
+ </td>
+ </ng-container>
- <tr mat-header-row *matHeaderRowDef="displayedColumnsExport"></tr>
- <tr
- mat-row
- *matRowDef="let row; columns: displayedColumnsExport"
- ></tr>
- </table>
+ <tr
+ mat-header-row
+ *matHeaderRowDef="displayedColumnsExport"
+ ></tr>
+ <tr
+ mat-row
+ *matRowDef="let row; columns: displayedColumnsExport"
+ ></tr>
+ </table>
- <div
- *ngIf="
- !availableExportProvider || availableExportProvider.length === 0
- "
- >
- <h5>{{ 'no stored export providers' | translate }}</h5>
- </div>
+ <div
+ *ngIf="
+ !availableExportProvider ||
+ availableExportProvider.length === 0
+ "
+ >
+ <h5>{{ 'no stored export providers' | translate }}</h5>
+ </div>
+ }
</div>
</sp-basic-view>
diff --git
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
index dd3677e50f..4c45f739cc 100644
---
a/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
+++
b/ui/src/app/dataset/components/datalake-configuration/datalake-configuration.component.ts
@@ -31,13 +31,17 @@ import {
ExportProviderSettings,
ExportProviderService,
RetentionLog,
+ UserService,
+ DataLakeMeasure,
} from '@streampipes/platform-services';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import {
+ CurrentUserService,
DataDownloadDialogComponent,
DialogRef,
DialogService,
+ ObjectPermissionDialogComponent,
LocalStorageService,
PanelType,
SpBreadcrumbService,
@@ -51,6 +55,8 @@ import { DeleteExportProviderComponent } from
'../../dialog/delete-export-provid
import { TranslateService } from '@ngx-translate/core';
import { ExportProviderConnectionTestComponent } from
'../../dialog/export-provider-connection-test/export-provider-connection-test.component';
import { DataRetentionLogDialogComponent } from
'../../dialog/data-retention-log-dialog/data-retention-log-dialog.component';
+import { UserRole } from 'src/app/_enums/user-role.enum';
+import { UserPrivilege } from 'src/app/_enums/user-privilege.enum';
@Component({
selector: 'sp-datalake-configuration',
@@ -70,6 +76,7 @@ export class DatalakeConfigurationComponent implements
OnInit, AfterViewInit {
private breadcrumbService = inject(SpBreadcrumbService);
private exportProviderRestService = inject(ExportProviderService);
private translateService = inject(TranslateService);
+ private currentUserService = inject(CurrentUserService);
dataSource: MatTableDataSource<DataLakeConfigurationEntry> =
new MatTableDataSource([]);
@@ -101,6 +108,8 @@ export class DatalakeConfigurationComponent implements
OnInit, AfterViewInit {
pageSize = this.localStorageService.get('paginator-page-size', 10);
pageIndex = 0;
+ isAdmin = false;
+ writeAccess = false;
ngOnInit(): void {
this.breadcrumbService.updateBreadcrumb([
@@ -109,6 +118,11 @@ export class DatalakeConfigurationComponent implements
OnInit, AfterViewInit {
]);
this.loadAvailableMeasurements();
this.loadAvailableExportProvider();
+ const currentUser = this.currentUserService.getCurrentUser();
+ this.isAdmin = currentUser.roles.indexOf(UserRole.ROLE_ADMIN) > -1;
+ this.writeAccess =
+ currentUser.roles.indexOf(UserPrivilege.PRIVILEGE_WRITE_DATASET) >
+ -1 || this.isAdmin;
}
ngAfterViewInit() {
@@ -348,6 +362,20 @@ export class DatalakeConfigurationComponent implements
OnInit, AfterViewInit {
this.queryEntryCounts(measurements, 'eventsLatest', 7);
}
}
+ showPermissionsDialog(element: DataLakeMeasure) {
+ this.dialogService.open(ObjectPermissionDialogComponent, {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: this.translateService.instant('Manage permissions'),
+ width: '50vw',
+ data: {
+ objectInstanceId: element.elementId,
+ headerTitle:
+ this.translateService.instant(
+ 'Manage permissions for dataset ',
+ ) + element.measureName,
+ },
+ });
+ }
queryEntryCounts(
measurements: string[],