Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ step-controller/step-controller-base-plugins/resources/
/step-cli/step-cli-launcher/src/test/resources/samples/
work/
/step-cli/step-cli-core/src/test/resources/testReports/
**/.claude
dependency-reduced-pom.xml
**/.claude
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<maven.compiler.release>11</maven.compiler.release>
<!-- internal dependencies -->
<step-grid.version>0.0.0-MASTER-SNAPSHOT</step-grid.version>
<step-framework.version>0.0.0-MASTER-SNAPSHOT</step-framework.version>
<step-framework.version>0.0.0-SED-4417-SNAPSHOT</step-framework.version>

<!-- external, non-transitive, dependencies -->
<dep.groovy.version>5.0.4</dep.groovy.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
{
"id": "69b010aeec94534eb48176db",
"name": "Step Main Layout",
"layout": {
"widgets": [
{
"widgetType": "errorsWidget",
"position": {
"row": 1,
"column": 1,
"widthInCells": 8,
"heightInCells": 1
}
},
{
"widgetType": "testCases",
"position": {
"row": 2,
"column": 1,
"widthInCells": 6,
"heightInCells": 3
}
},
{
"widgetType": "testCasesSummary",
"position": {
"row": 2,
"column": 7,
"widthInCells": 2,
"heightInCells": 3
}
},
{
"widgetType": "keywordsSummary",
"position": {
"row": 5,
"column": 1,
"widthInCells": 2,
"heightInCells": 3
}
},
{
"widgetType": "executionTree",
"position": {
"row": 5,
"column": 3,
"widthInCells": 6,
"heightInCells": 3
}
},
{
"widgetType": "keywordsList",
"position": {
"row": 8,
"column": 1,
"widthInCells": 6,
"heightInCells": 3
}
},
{
"widgetType": "performanceOverview",
"position": {
"row": 8,
"column": 7,
"widthInCells": 2,
"heightInCells": 3
}
},
{
"widgetType": "errors",
"position": {
"row": 11,
"column": 1,
"widthInCells": 8,
"heightInCells": 3
}
},
{
"widgetType": "notificationSubscriptionForExecution",
"position": {
"row": 14,
"column": 1,
"widthInCells": 4,
"heightInCells": 3
}
},
{
"widgetType": "housekeepingSettingsForExecution",
"position": {
"row": 14,
"column": 5,
"widthInCells": 4,
"heightInCells": 3
}
},
{
"widgetType": "currentOperations",
"position": {
"row": 17,
"column": 1,
"widthInCells": 4,
"heightInCells": 3
}
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,8 @@ plugins.FunctionPackagePlugin.maven.repository.central.url=https://repo1.maven.o
#timeseries.collections.week.flush.period=3600000
#timeseries.response.intervals.ideal=100
#timeseries.response.intervals.max=1000
#-----------------------------------------------------
# Reporting
#-----------------------------------------------------
plugins.reporting.layouts.presets.folder=src/test/resources/reporting/layouts/presets
plugins.reporting.layouts.default.id=69b010aeec94534eb48176db
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import step.automation.packages.AutomationPackageEntity;
import step.controller.services.async.AsyncTaskStatus;
import step.core.GlobalContext;
import step.core.access.User;
import step.core.accessors.AbstractIdentifiableObject;
import step.core.accessors.AbstractOrganizableObject;
import step.core.accessors.AbstractTrackedObject;
Expand Down Expand Up @@ -152,11 +153,19 @@ private void trackEntityIfApplicable(T entity) {
AbstractTrackedObject newTrackedEntity = (AbstractTrackedObject) entity;
ObjectId sourceId = entity.getId();
T sourceEntity = (sourceId != null) ? accessor.get(sourceId) : null;
String username = getSession().getUser().getUsername();
User user = getSession().getUser();
String username = user.getUsername();
String userId = user.getId().toHexString();
Date lastModificationDate = new Date();
if (sourceEntity == null) {
//If source is null or not an instance of AbstractTrackedObject, we set creation metadata
if (!(sourceEntity instanceof AbstractTrackedObject)) {
newTrackedEntity.setCreationDate(lastModificationDate);
newTrackedEntity.setCreationUser(username);
} else {
//In case of update we make sure we keep the creation metadata from the DB info
AbstractTrackedObject sourceTrackedEntity = (AbstractTrackedObject) sourceEntity;
newTrackedEntity.setCreationDate(sourceTrackedEntity.getCreationDate());
newTrackedEntity.setCreationUser(sourceTrackedEntity.getCreationUser());
}
newTrackedEntity.setLastModificationDate(lastModificationDate);
newTrackedEntity.setLastModificationUser(username);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package step.core.reporting;

import step.core.accessors.AbstractAccessor;
import step.core.accessors.AbstractOrganizableObject;
import step.core.collections.Collection;
import step.core.collections.Filters;
import step.core.collections.SearchOrder;
import step.core.collections.filters.Or;
import step.core.reporting.model.ReportLayout;

import java.util.List;
import java.util.stream.Collectors;

import static step.core.collections.Order.ASC;
import static step.core.reporting.model.ReportLayout.FIELD_VISIBILITY;
import static step.core.reporting.model.ReportLayout.ReportLayoutVisibility.Preset;
import static step.core.reporting.model.ReportLayout.ReportLayoutVisibility.Shared;

public class ReportLayoutAccessor extends AbstractAccessor<ReportLayout> {

public ReportLayoutAccessor(Collection<ReportLayout> collectionDriver) {
super(collectionDriver);
}

public List<ReportLayout> getAccessibleReportLayoutsDefinitions(String userName) {
Or ownerOrShared = Filters.or(List.of(Filters.equals(FIELD_VISIBILITY, Preset.name()), Filters.equals(FIELD_VISIBILITY, Shared.name()), Filters.equals("creationUser", userName)));
return this.getCollectionDriver()
.find(ownerOrShared, new SearchOrder(ATTRIBUTES_FIELD_NAME + "." + AbstractOrganizableObject.NAME, ASC.numeric), null, null, 0)
.peek(reportLayout -> reportLayout.layout = null)
.collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package step.core.reporting;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import step.core.GlobalContext;
import step.core.accessors.AbstractOrganizableObject;
import step.core.accessors.DefaultJacksonMapperProvider;
import step.core.collections.Collection;
import step.core.collections.Filters;
import step.core.deployment.WebApplicationConfigurationManager;
import step.core.entities.Entity;
import step.core.entities.EntityConstants;
import step.core.plugins.AbstractControllerPlugin;
import step.core.plugins.Plugin;
import step.core.reporting.model.ReportLayout;
import step.core.reporting.model.ReportLayoutJson;
import step.framework.server.tables.Table;
import step.framework.server.tables.TableRegistry;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Plugin
public class ReportLayoutPlugin extends AbstractControllerPlugin {

private static final Logger logger = LoggerFactory.getLogger(ReportLayoutPlugin.class);

public static final String DEFAULT_REPORT_LAYOUT = "Default";
public static final String PRESET_FOLDER_PATH_CONFIG_KEY = "plugins.reporting.layouts.presets.folder";
public static final String PRESET_FOLDER_PATH_DEFAULT = "../plugins/reporting/layouts";
public static final String DEFAULT_LAYOUT_ID_CONFIG_KEY = "plugins.reporting.layouts.default.id";
public static final String DEFAULT_LAYOUT_ID_DEFAULT = "69b010aeec94534eb48176db";

private ReportLayoutAccessor reportLayoutAccessor;
private String defaultLayoutId;

@Override
public void serverStart(GlobalContext context) throws Exception {
super.serverStart(context);
TableRegistry tableRegistry = context.require(TableRegistry.class);
//Create accessor
Collection<ReportLayout> reportLayoutCollection = context.getCollectionFactory().getCollection(EntityConstants.reportLayouts, ReportLayout.class);
reportLayoutAccessor = new ReportLayoutAccessor(reportLayoutCollection);
context.put(ReportLayoutAccessor.class, reportLayoutAccessor);
//Register entity
context.getEntityManager().register(new Entity<>(EntityConstants.reportLayouts, reportLayoutAccessor, ReportLayout.class));
//Register Table, table only return layout metadata not the layout itself
tableRegistry.register(EntityConstants.reportLayouts, new Table<>(reportLayoutCollection, ReportLayoutServices.REPORT_LAYOUT_RIGHT + "-read", false).withResultItemTransformer((reportLayout, session) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a table?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply to be coherent with other entities and be ready if a managed layouts view is required in the future. Do you see any issue keeping it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. If we'll anyway need it in the future it doesn't hurt. Concerning the gemini comment below, it is not a security issue as we remove the content

reportLayout.layout = null;
return reportLayout;
}));
Comment on lines +52 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The table for reportLayouts is registered without any default filters to restrict access based on ownership or sharing status. This allows any user with the reportLayout-read permission to view the metadata (such as name, owner, and attributes) of all layouts in the system via the /table endpoint, bypassing the ownership filtering implemented in the /list endpoint. While the layout field itself is cleared, the exposure of metadata for private layouts is still a security concern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table is created with restriction for the report layout read right, only authenticated user with this specific right can access the list of layout (stripped from the actual layout content). I don't see it as an issue in this case.

//Register Services
context.getServiceRegistrationCallback().registerService(ReportLayoutServices.class);

//Add default layout ID to UI configuration
defaultLayoutId = context.getConfiguration().getProperty(DEFAULT_LAYOUT_ID_CONFIG_KEY, DEFAULT_LAYOUT_ID_DEFAULT);
WebApplicationConfigurationManager configurationManager = context.require(WebApplicationConfigurationManager.class);
configurationManager.registerHook(s -> Map.of(DEFAULT_LAYOUT_ID_CONFIG_KEY, defaultLayoutId));

}

@Override
public void initializeData(GlobalContext context) throws Exception {
super.initializeData(context);
// Drop all existing presets - the folder is the source of truth at startup
reportLayoutAccessor.getCollectionDriver().remove(
Filters.equals(ReportLayout.FIELD_VISIBILITY, ReportLayout.ReportLayoutVisibility.Preset.name()));

// Load presets from the configured folder
File presetsFolder = new File(context.getConfiguration().getProperty(PRESET_FOLDER_PATH_CONFIG_KEY, PRESET_FOLDER_PATH_DEFAULT));
if (presetsFolder.exists() && presetsFolder.isDirectory()) {
ObjectMapper objectMapper = DefaultJacksonMapperProvider.getObjectMapper();
File[] jsonFiles = presetsFolder.listFiles((dir, name) -> name.endsWith(".json"));
if (jsonFiles != null) {
for (File jsonFile : jsonFiles) {
try {
ReportLayoutJson layoutJson = objectMapper.readValue(jsonFile, ReportLayoutJson.class);
if (ObjectId.isValid(layoutJson.id)) {
ReportLayout reportLayout = new ReportLayout(layoutJson.layout, ReportLayout.ReportLayoutVisibility.Preset);
reportLayout.addAttribute(AbstractOrganizableObject.NAME, layoutJson.name);
reportLayout.setId(new ObjectId(layoutJson.id));
reportLayoutAccessor.save(reportLayout);
} else {
logger.error("Invalid json file: {}, the id {} has been tempered with and is not a valid ObjectId", jsonFile.getAbsolutePath(), layoutJson.id);
}
} catch (Exception e) {
logger.error("Failed to load preset layout from file '{}'", jsonFile.getAbsolutePath(), e);
}
}
}
} else {
logger.warn("The configured presets folder '{}' does not exist or is not a directory.", presetsFolder.getAbsolutePath());
}
}
}
Loading