diff --git a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/component/preview/PreviewChangesTabPanel.java b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/component/preview/PreviewChangesTabPanel.java index e36c7d917eb..7b7b6581436 100644 --- a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/component/preview/PreviewChangesTabPanel.java +++ b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/component/preview/PreviewChangesTabPanel.java @@ -6,6 +6,8 @@ package com.evolveum.midpoint.gui.impl.page.admin.component.preview; +import java.io.Serial; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -17,12 +19,13 @@ import com.evolveum.midpoint.gui.api.component.BasePanel; import com.evolveum.midpoint.gui.api.model.LoadableModel; +import com.evolveum.midpoint.gui.api.page.PageBase; import com.evolveum.midpoint.gui.api.util.WebComponentUtil; import com.evolveum.midpoint.gui.impl.page.admin.focus.PageFocusPreviewChanges; import com.evolveum.midpoint.model.api.context.ModelContext; import com.evolveum.midpoint.model.api.visualizer.ModelContextVisualization; import com.evolveum.midpoint.model.api.visualizer.Visualization; -import com.evolveum.midpoint.repo.common.ObjectResolver; +import com.evolveum.midpoint.schema.TaskExecutionMode; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.DebugUtil; @@ -47,7 +50,7 @@ import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.PolicyRuleEnforcerPreviewOutputType; -public class PreviewChangesTabPanel extends BasePanel> { +public class PreviewChangesTabPanel extends BasePanel { private static final long serialVersionUID = 1L; private static final String ID_PRIMARY_DELTAS = "primaryDeltas"; @@ -64,10 +67,13 @@ public class PreviewChangesTabPanel extends BasePanel> policyViolationsModel; private IModel> approvalsModel; + private final PreviewData data; + private static final Trace LOGGER = TraceManager.getTrace(PreviewChangesTabPanel.class); - public PreviewChangesTabPanel(String id, IModel> contextModel) { - super(id, contextModel); + public PreviewChangesTabPanel(String id, PreviewData data) { + super(id); + this.data = data; } @Override @@ -78,15 +84,28 @@ protected void onInitialize() { initLayout(); } + /** Initializes Wicket models from already extracted serializable preview data. */ private void initModels() { + primaryModel = Model.ofList(data.primary()); + secondaryModel = Model.ofList(data.secondary()); + policyViolationsModel = Model.ofList(data.policyViolations()); + approvalsModel = Model.ofList(data.approvals()); + } + + /** + * Extracts the data needed by the preview page from live model contexts. + * + * The returned data is safe to keep in Wicket page state; the original model contexts are not. + */ + public static PreviewData createPreviewData( + String title, ModelContext modelContext, PageBase pageBase) { ModelContextVisualization mcVisualization; - ModelContext modelContext = getModelObject(); try { - Task task = getPageBase().createSimpleTask("visualize"); + Task task = pageBase.createSimpleTask("visualize"); OperationResult result = task.getResult(); - mcVisualization = getPageBase().getModelInteractionService().visualizeModelContext(modelContext, task, result); + mcVisualization = pageBase.getModelInteractionService().visualizeModelContext(modelContext, task, result); } catch (SchemaException | ExpressionEvaluationException | ConfigurationException e) { throw new SystemException(e); // TODO } @@ -102,41 +121,46 @@ private void initModels() { final List primaryList = primary.stream().map(v -> new VisualizationDto(v)).collect(Collectors.toList()); final List secondaryList = secondary.stream().map(v -> new VisualizationDto(v)).collect(Collectors.toList()); - primaryModel = () -> primaryList; - secondaryModel = () -> secondaryList; - PolicyRuleEnforcerPreviewOutputType enforcements = modelContext != null ? modelContext.getPolicyRuleEnforcerPreviewOutput() : null; List triggerGroups = enforcements != null ? Collections.singletonList(EvaluatedTriggerGroupDto.initializeFromRules(enforcements.getRule(), false, null)) : Collections.emptyList(); - policyViolationsModel = Model.ofList(triggerGroups); List approvalsExecutionList = modelContext != null ? modelContext.getHookPreviewResults(ApprovalSchemaExecutionInformationType.class) : Collections.emptyList(); List approvals = new ArrayList<>(); if (!approvalsExecutionList.isEmpty()) { - Task opTask = getPageBase().createSimpleTask(PageFocusPreviewChanges.class + ".createApprovals"); // TODO + Task opTask = pageBase.createSimpleTask(PageFocusPreviewChanges.class + ".createApprovals"); // TODO OperationResult result = opTask.getResult(); try { for (ApprovalSchemaExecutionInformationType execution : approvalsExecutionList) { approvals.add(ApprovalProcessExecutionInformationDto .createFrom(execution, true, opTask, result, - PreviewChangesTabPanel.this.getPageBase())); // TODO reuse session + pageBase)); // TODO reuse session } result.computeStatus(); } catch (Throwable t) { LoggingUtils.logUnexpectedException(LOGGER, "Couldn't prepare approval information", t); result.recordFatalError( - createStringResource("PreviewChangesTabPanel.message.prepareApproval.fatalError", t.getMessage()).getString(), t); + pageBase.createStringResource( + "PreviewChangesTabPanel.message.prepareApproval.fatalError", t.getMessage()).getString(), t); } if (WebComponentUtil.showResultInPage(result)) { - getPageBase().showResult(result); + pageBase.showResult(result); } } - approvalsModel = Model.ofList(approvals); + + return new PreviewData( + title, + primaryList, + secondaryList, + triggerGroups, + approvals, + modelContext != null + && TaskExecutionMode.SIMULATED_PRODUCTION.equals(modelContext.getTaskExecutionMode())); } private IModel createVisualizationModel(IModel> model, String oneKey, String moreKey) { @@ -208,4 +232,25 @@ private boolean approvalsEmpty() { private boolean violationsEmpty() { return EvaluatedTriggerGroupDto.isEmpty(policyViolationsModel.getObject()); } + + /** + * Serializable page-state representation of one preview tab. + * + * It is created once while rendering the preview page and then reused by tab panels + * and button visibility checks during later Wicket requests. + */ + public record PreviewData( + String title, + List primary, + List secondary, + List policyViolations, + List approvals, + boolean withProductionConfiguration) implements Serializable { + + @Serial private static final long serialVersionUID = 1L; + + public boolean violationsEmpty() { + return EvaluatedTriggerGroupDto.isEmpty(policyViolations); + } + } } diff --git a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/focus/PageFocusPreviewChanges.java b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/focus/PageFocusPreviewChanges.java index 51b55e311ba..25a46c1417f 100644 --- a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/focus/PageFocusPreviewChanges.java +++ b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/focus/PageFocusPreviewChanges.java @@ -7,12 +7,9 @@ package com.evolveum.midpoint.gui.impl.page.admin.focus; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; -import com.evolveum.midpoint.schema.TaskExecutionMode; - import org.apache.wicket.Component; import org.apache.wicket.RestartResponseException; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -30,6 +27,7 @@ import com.evolveum.midpoint.gui.api.page.PageBase; import com.evolveum.midpoint.gui.api.util.WebComponentUtil; import com.evolveum.midpoint.gui.impl.page.admin.component.preview.PreviewChangesTabPanel; +import com.evolveum.midpoint.gui.impl.page.admin.component.preview.PreviewChangesTabPanel.PreviewData; import com.evolveum.midpoint.model.api.context.ModelContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.security.api.AuthorizationConstants; @@ -40,11 +38,9 @@ import com.evolveum.midpoint.web.component.breadcrumbs.Breadcrumb; import com.evolveum.midpoint.web.component.form.MidpointForm; import com.evolveum.midpoint.web.component.util.VisibleBehaviour; -import com.evolveum.midpoint.web.page.admin.workflow.dto.EvaluatedTriggerGroupDto; import com.evolveum.midpoint.web.util.OnePageParameterEncoder; import com.evolveum.midpoint.xml.ns._public.common.common_3.AbstractRoleType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; -import com.evolveum.midpoint.xml.ns._public.common.common_3.PolicyRuleEnforcerPreviewOutputType; @PageDescriptor( urls = { @@ -65,7 +61,13 @@ public class PageFocusPreviewChanges extends PageBase { private static final Trace LOGGER = TraceManager.getTrace(PageFocusPreviewChanges.class); - private Map, ModelContext> modelContextMap; + private List previewData; + /* + * Live model contexts may contain non-serializable model execution state + * (e.g. LensContext -> MagicAssignment/Holder). Keep them only as transient + * input until serializable preview data is created. + */ + private transient Map, ModelContext> modelContextMap; private PageBase previousPage; @@ -78,9 +80,28 @@ public PageFocusPreviewChanges(Map, ModelContext> modelContext this.previousPage = previousPage; } + /** + * Extracts the data needed by the preview page from live model contexts. + * + * The returned data is safe to keep in Wicket page state; the original model contexts are not. + */ + private List createPreviewData(Map, ModelContext> modelContextMap) { + List previewData = new ArrayList<>(); + modelContextMap.forEach((object, modelContext) -> + previewData.add( + PreviewChangesTabPanel.createPreviewData( + getTabPanelTitleModel(object).getObject(), modelContext, this))); + return previewData; + } + @Override protected void onInitialize() { super.onInitialize(); + // Wicket page state must contain only the serializable preview representation. + if (previewData == null) { + previewData = createPreviewData(modelContextMap); + modelContextMap = null; + } initLayout(); } @@ -129,24 +150,17 @@ public void onClick(AjaxRequestTarget target) { } private boolean isWithProductionConfiguration() { - for (ModelContext modelContext : modelContextMap.values()) { - if (modelContext != null && TaskExecutionMode.SIMULATED_PRODUCTION.equals(modelContext.getTaskExecutionMode())) { + for (PreviewData previewChange : previewData) { + if (previewChange.withProductionConfiguration()) { return true; } } return false; } - //TODO relocate the logic from the loop to some util method, code repeats in PreviewChangesTabPanel private boolean violationsEmpty() { - for (ModelContext modelContext : modelContextMap.values()) { - PolicyRuleEnforcerPreviewOutputType enforcements = modelContext != null - ? modelContext.getPolicyRuleEnforcerPreviewOutput() - : null; - List triggerGroups = enforcements != null - ? Collections.singletonList(EvaluatedTriggerGroupDto.initializeFromRules(enforcements.getRule(), false, null)) - : Collections.emptyList(); - if (!EvaluatedTriggerGroupDto.isEmpty(triggerGroups)) { + for (PreviewData previewChange : previewData) { + if (!previewChange.violationsEmpty()) { return false; } } @@ -155,15 +169,15 @@ private boolean violationsEmpty() { private List createTabs() { List tabs = new ArrayList<>(); - modelContextMap.forEach((object, modelContext) -> { + previewData.forEach(previewChange -> { - tabs.add(new PanelTab(getTabPanelTitleModel(object)) { + tabs.add(new PanelTab(Model.of(previewChange.title())) { private static final long serialVersionUID = 1L; @Override public WebMarkupContainer createPanel(String panelId) { - return new PreviewChangesTabPanel(panelId, Model.of(modelContext)); + return new PreviewChangesTabPanel(panelId, previewChange); } }); }); diff --git a/gui/admin-gui/src/test/java/com/evolveum/midpoint/gui/TestPageFocusPreviewChanges.java b/gui/admin-gui/src/test/java/com/evolveum/midpoint/gui/TestPageFocusPreviewChanges.java new file mode 100644 index 00000000000..b43bcf605f2 --- /dev/null +++ b/gui/admin-gui/src/test/java/com/evolveum/midpoint/gui/TestPageFocusPreviewChanges.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2026 Evolveum and contributors + * + * Licensed under the EUPL-1.2 or later. + */ + +package com.evolveum.midpoint.gui; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import static com.evolveum.midpoint.test.util.MidPointAsserts.assertSerializable; + +import java.io.File; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.wicket.Component; +import org.apache.wicket.Page; +import org.apache.wicket.model.IModel; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ActiveProfiles; +import org.testng.annotations.Test; + +import com.evolveum.midpoint.gui.impl.page.admin.component.preview.PreviewChangesTabPanel; +import com.evolveum.midpoint.gui.impl.page.admin.focus.PageFocusPreviewChanges; +import com.evolveum.midpoint.gui.test.TestMidPointSpringApplication; +import com.evolveum.midpoint.model.api.ModelExecuteOptions; +import com.evolveum.midpoint.model.api.context.EvaluatedAssignment; +import com.evolveum.midpoint.model.api.context.ModelContext; +import com.evolveum.midpoint.prism.PrismObject; +import com.evolveum.midpoint.prism.delta.ObjectDelta; +import com.evolveum.midpoint.schema.result.OperationResult; +import com.evolveum.midpoint.task.api.Task; +import com.evolveum.midpoint.web.AbstractInitializedGuiIntegrationTest; +import com.evolveum.midpoint.xml.ns._public.common.common_3.AssignmentType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.OrgType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.RoleType; + +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +@ActiveProfiles("test") +@SpringBootTest(classes = TestMidPointSpringApplication.class) +public class TestPageFocusPreviewChanges extends AbstractInitializedGuiIntegrationTest { + + private static final File ORG_PREVIEW_POLICY_RULE_FILE = + new File("src/test/resources/common/org-preview-policy-rule.xml"); + private static final String ORG_PREVIEW_ROLE_OID = "11111111-1111-1111-1111-000000110082"; + private static final String ORG_PREVIEW_ORG_OID = "33333333-3333-3333-3333-000000110082"; + + @Override + public void initSystem(Task initTask, OperationResult initResult) throws Exception { + super.initSystem(initTask, initResult); + importObjectFromFile(ORG_PREVIEW_POLICY_RULE_FILE, initTask, initResult); + } + + @Test + public void test001PreviewChangesPageSerializationWithPolicyRuleContext() throws Exception { + Task task = getTestTask(); + OperationResult result = task.getResult(); + + ObjectDelta delta = prismContext.deltaFor(OrgType.class) + .item(OrgType.F_ASSIGNMENT) + .add(new AssignmentType() + .targetRef(ORG_PREVIEW_ROLE_OID, RoleType.COMPLEX_TYPE)) + .asObjectDelta(ORG_PREVIEW_ORG_OID); + + ModelContext modelContext = modelInteractionService.previewChanges( + List.of(delta), ModelExecuteOptions.create().reconcile(), task, result); + assertSuccess(result); + assertPolicyRuleFocusMappingContext(modelContext); + + PrismObject org = modelService.getObject(OrgType.class, ORG_PREVIEW_ORG_OID, null, task, result); + PageFocusPreviewChanges page = new PageFocusPreviewChanges<>(Map.of(org, modelContext), null); + + tester.startPage(page); + tester.assertRenderedPage(PageFocusPreviewChanges.class); + + Page renderedPage = tester.getLastRenderedPage(); + assertNotNull( + tester.getApplication().getFrameworkSettings().getSerializer().serialize(renderedPage), + "Rendered preview page is not Wicket-serializable"); + assertNoPreviewTabPanelRetainsModelContext(renderedPage); + assertSerializable(renderedPage); + } + + private void assertPolicyRuleFocusMappingContext(ModelContext modelContext) { + assertFalse( + modelContext.getFocusContextRequired().getObjectPolicyRules().isEmpty(), + "Policy rule from the archetype was not evaluated"); + assertTrue( + modelContext.getAllEvaluatedAssignments().stream() + .anyMatch(this::hasFocusMappingRequestWithMagicAssignment), + "No evaluated assignment contains focus mapping request with magic assignment"); + } + + private boolean hasFocusMappingRequestWithMagicAssignment(EvaluatedAssignment evaluatedAssignment) { + try { + Method method = evaluatedAssignment.getClass().getMethod("getFocusMappingEvaluationRequests"); + Collection requests = (Collection) method.invoke(evaluatedAssignment); + for (Object request : requests) { + Object variables = request.getClass().getMethod("getAssignmentPathVariables").invoke(request); + Object magicAssignment = variables.getClass().getMethod("getMagicAssignment").invoke(variables); + if (magicAssignment != null) { + return true; + } + } + } catch (ReflectiveOperationException e) { + return false; + } + return false; + } + + private void assertNoPreviewTabPanelRetainsModelContext(Page renderedPage) { + AtomicInteger previewTabPanels = new AtomicInteger(); + renderedPage.visitChildren(PreviewChangesTabPanel.class, (panel, visit) -> { + previewTabPanels.incrementAndGet(); + IModel model = ((Component) panel).getDefaultModel(); + assertTrue(model == null || !(model.getObject() instanceof ModelContext), + "PreviewChangesTabPanel must not retain ModelContext in Wicket page state"); + }); + assertTrue(previewTabPanels.get() > 0, "PreviewChangesTabPanel was not rendered"); + } +} diff --git a/gui/admin-gui/src/test/resources/common/org-preview-policy-rule.xml b/gui/admin-gui/src/test/resources/common/org-preview-policy-rule.xml new file mode 100644 index 00000000000..b492f16d053 --- /dev/null +++ b/gui/admin-gui/src/test/resources/common/org-preview-policy-rule.xml @@ -0,0 +1,60 @@ + + + + + + + Preview policy normal role + + + + Preview policy Org archetype + + + + OrgType + + + + + + + force assignment path variables + + Preview policy focus mapping fired + + + description + + + + + + + + roleMembershipRef + + + + + + + + OrgType + + + + + Preview policy org + + + + + + diff --git a/gui/admin-gui/testng-integration.xml b/gui/admin-gui/testng-integration.xml index 1e5394d1675..aa1fb6daeb8 100644 --- a/gui/admin-gui/testng-integration.xml +++ b/gui/admin-gui/testng-integration.xml @@ -20,6 +20,7 @@ + diff --git a/release-notes.adoc b/release-notes.adoc index 10797a17f9d..8926a83c72f 100644 --- a/release-notes.adoc +++ b/release-notes.adoc @@ -105,6 +105,7 @@ Overall, midPoint 4.10 opens up the world of identity management and governance * Fix 10k limit in certification queries using iterative search. See bug:MID-11043[] * Fixed generic "Fatal error" message in object tables to show available list/search error details. See bug:MID-10911[] * Improved user list performance with orgRelation authorization for users managing many organizations. See bug:MID-11179[] +* Fixed serialization of focus preview changes page by avoiding retained live model context. See bug:MID-11082[] === Releases Of Other Components