From 591c853cc4ae68e63f0b6bb518cd891293fe3fae Mon Sep 17 00:00:00 2001 From: det101 <3656843+det101@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:45:00 +0800 Subject: [PATCH 1/7] [DSIP-105][API] Encrypt sensitive definition params with PasswordUtils --- .../impl/WorkflowDefinitionServiceImpl.java | 10 +- .../impl/WorkflowInstanceServiceImpl.java | 2 +- .../api/utils/SensitivePropertyUtils.java | 99 +++++++++++++++++-- .../api/utils/SensitivePropertyUtilsTest.java | 90 +++++++++++++++++ .../handler/RunWorkflowCommandHandler.java | 4 +- .../TaskExecutionContextBuilder.java | 4 +- .../runner/TaskExecutionContextFactory.java | 3 +- .../utils/SensitivePropertyCryptoUtils.java | 43 ++++++++ .../SensitivePropertyCryptoUtilsTest.java | 76 ++++++++++++++ .../api/utils/PropertySensitiveUtils.java | 49 +++++++++ .../api/utils/PropertySensitiveUtilsTest.java | 21 ++++ 11 files changed, 387 insertions(+), 14 deletions(-) create mode 100644 dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java create mode 100644 dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtilsTest.java diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java index 730152ff1340..ad6ac3b962ff 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java @@ -264,7 +264,13 @@ public WorkflowDefinition createWorkflowDefinition(User loginUser, List taskDefinitionLogs = generateTaskDefinitionList(taskDefinitionJson); List taskRelationList = generateTaskRelationList(taskRelationJson, taskDefinitionLogs); - SensitivePropertyUtils.requireNoPlaceholder(GlobalParameterUtils.deserializeGlobalParameter(globalParams)); + List submittedGlobalParams = GlobalParameterUtils.deserializeGlobalParameter(globalParams); + if (CollectionUtils.isNotEmpty(submittedGlobalParams)) { + globalParams = GlobalParameterUtils.serializeGlobalParameter( + SensitivePropertyUtils.encodeForCreate(submittedGlobalParams)); + } else { + SensitivePropertyUtils.requireNoPlaceholder(submittedGlobalParams); + } for (TaskDefinitionLog taskDefinitionLog : CollectionUtils.emptyIfNull(taskDefinitionLogs)) { taskDefinitionLog.setTaskParams( SensitivePropertyUtils.mergeLocalParams(taskDefinitionLog.getTaskParams(), null)); @@ -667,7 +673,7 @@ public WorkflowDefinition updateWorkflowDefinition(User loginUser, GlobalParameterUtils.deserializeGlobalParameter(globalParams); if (CollectionUtils.isNotEmpty(submittedGlobalParams)) { globalParams = GlobalParameterUtils.serializeGlobalParameter( - SensitivePropertyUtils.merge(submittedGlobalParams, + SensitivePropertyUtils.mergeAndEncode(submittedGlobalParams, GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()))); } List versionKeys = new ArrayList<>(); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java index bf4356d1f46e..b22dec1def5f 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java @@ -449,7 +449,7 @@ public WorkflowDefinition updateWorkflowInstance(User loginUser, long projectCod ? null : existingTaskParamsMap.get(submitted.getCode() + "_" + submitted.getVersion()); submitted.setTaskParams(SensitivePropertyUtils.mergeLocalParams( - submitted.getTaskParams(), existingTaskParams)); + submitted.getTaskParams(), existingTaskParams, false)); } taskDatasourcePermissionChecker.checkPermission(loginUser, taskDefinitionLogs); taskSubWorkflowPermissionChecker.checkPermission(loginUser, taskDefinitionLogs); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java index a94766f1747b..83069b0f0b01 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java @@ -36,6 +36,7 @@ import org.apache.dolphinscheduler.dao.entity.WorkflowInstance; import org.apache.dolphinscheduler.extract.master.command.AbstractCommandParam; import org.apache.dolphinscheduler.extract.master.command.ICommandParam; +import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; import org.apache.dolphinscheduler.plugin.task.api.model.Property; import org.apache.dolphinscheduler.plugin.task.api.utils.GlobalParameterUtils; import org.apache.dolphinscheduler.plugin.task.api.utils.PropertySensitiveUtils; @@ -62,9 +63,8 @@ /** * HTTP-layer helpers for {@link Property#isSensitive()}. - * Values stay plaintext in DB. Query masks them as {@code ******}. - * Create rejects {@code ******}; Update {@link #merge}s it back to the old value. - * Start: {@code ******} is replaced with the definition global plaintext. + * Definition write path: merge keep-original then encode with {@link PasswordUtils} when enabled. + * Query masks values as {@code ******}. Start / execution use decrypt copies. */ @UtilityClass public class SensitivePropertyUtils { @@ -80,9 +80,27 @@ public void requireNoPlaceholder(List properties) { } /** - * Update: restore DB plaintext for keep-original {@code ******}. + * Create definition params: reject {@code ******}, then encode sensitive plaintext. + */ + public List encodeForCreate(List properties) { + requireNoPlaceholder(properties); + return encodeNewSensitivePlaintext(properties, Collections.emptyList()); + } + + /** + * Update definition params: restore DB value for keep-original {@code ******}, then encode. * Empty / null is a real empty value. {@code false→true} + {@code ******} is allowed; * {@code true→false} + {@code ******} is rejected. + * Runtime instance updates should call {@link #merge} only (no encode). + */ + public List mergeAndEncode(List submittedProperties, List existingProperties) { + List merged = merge(submittedProperties, existingProperties); + return encodeNewSensitivePlaintext(merged, existingProperties); + } + + /** + * Update: restore DB value for keep-original {@code ******} without encoding. + * Used for runtime instance global_params (plaintext materialization). */ public List merge(List submittedProperties, List existingProperties) { if (CollectionUtils.isEmpty(submittedProperties)) { @@ -100,6 +118,7 @@ public List merge(List submittedProperties, List e /** * Build start/command params before persist. + * Decrypts definition globals first, then: *
    *
  • Same name as a workflow global: inherit all global attributes, only override {@code value} * ({@code ******} keeps the global plaintext).
  • @@ -110,7 +129,8 @@ public List restoreStartParams(List startParams, List globals = CollectionUtils.emptyIfNull(globalParams).stream() + List decryptedGlobals = decodeSensitiveValues(globalParams); + Map globals = CollectionUtils.emptyIfNull(decryptedGlobals).stream() .filter(Objects::nonNull) .filter(property -> property.getProp() != null) .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); @@ -137,16 +157,79 @@ public List restoreStartParams(List startParams, List decodeSensitiveValues(List properties) { + return PropertySensitiveUtils.transformSensitiveValues(properties, PasswordUtils::decodePassword); + } + public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { + return mergeLocalParams(submittedTaskParams, existingTaskParams, true); + } + + /** + * @param encodeForDefinition when true, encode after merge (workflow/task definition persist); + * when false, merge only (runtime instance edit). + */ + public String mergeLocalParams(String submittedTaskParams, String existingTaskParams, + boolean encodeForDefinition) { return rewriteLocalParams(submittedTaskParams, submitted -> { if (StringUtils.isEmpty(existingTaskParams)) { - requireNoPlaceholder(submitted); - return submitted; + return encodeForDefinition ? encodeForCreate(submitted) : requireNoPlaceholderAndReturn(submitted); } - return merge(submitted, getLocalParams(existingTaskParams)); + List existing = getLocalParams(existingTaskParams); + return encodeForDefinition ? mergeAndEncode(submitted, existing) : merge(submitted, existing); }); } + private List requireNoPlaceholderAndReturn(List submitted) { + requireNoPlaceholder(submitted); + return submitted; + } + + /** + * Encode sensitive values that are new plaintext for definition persist. + * Keep-original from an already-sensitive existing value is written as-is (no re-encode). + * {@code false→true} keep-original merges non-sensitive plaintext and then encodes. + */ + private List encodeNewSensitivePlaintext(List mergedProperties, + List existingProperties) { + if (CollectionUtils.isEmpty(mergedProperties)) { + return mergedProperties; + } + Map existingMap = CollectionUtils.emptyIfNull(existingProperties).stream() + .filter(Objects::nonNull) + .filter(property -> property.getProp() != null) + .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); + List encoded = new ArrayList<>(mergedProperties.size()); + for (Property property : mergedProperties) { + Property copy = PropertySensitiveUtils.copy(property); + if (!PropertySensitiveUtils.isSensitive(copy)) { + Property existing = existingMap.get(copy.getProp()); + // true→false: if a non-UI client re-sent ciphertext, decode back to plaintext. + if (existing != null && existing.isSensitive() + && StringUtils.isNotEmpty(copy.getValue()) + && Objects.equals(copy.getValue(), existing.getValue())) { + copy.setValue(PasswordUtils.decodePassword(copy.getValue())); + } + encoded.add(copy); + continue; + } + if (StringUtils.isEmpty(copy.getValue())) { + encoded.add(copy); + continue; + } + Property existing = existingMap.get(copy.getProp()); + if (existing != null && existing.isSensitive() + && Objects.equals(copy.getValue(), existing.getValue())) { + // true→true keep-original (or identical resubmit): already persisted form. + encoded.add(copy); + continue; + } + copy.setValue(PasswordUtils.encodePassword(copy.getValue())); + encoded.add(copy); + } + return encoded; + } + /** * Copy then mask for HTTP responses. Do not mask Service return values in place. * Nested {@link DagData} is remasked from the original (BeanUtils is shallow). diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java index 368622976718..c7ad4176ca3b 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.api.exceptions.ServiceException; import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.common.utils.PropertyUtils; import org.apache.dolphinscheduler.dao.entity.Command; import org.apache.dolphinscheduler.dao.entity.ErrorCommand; import org.apache.dolphinscheduler.dao.entity.TaskInstance; @@ -27,6 +28,8 @@ import org.apache.dolphinscheduler.dao.entity.WorkflowInstance; import org.apache.dolphinscheduler.extract.master.command.ICommandParam; import org.apache.dolphinscheduler.extract.master.command.RunWorkflowCommandParam; +import org.apache.dolphinscheduler.plugin.datasource.api.constants.DataSourceConstants; +import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; import org.apache.dolphinscheduler.plugin.task.api.enums.DataType; import org.apache.dolphinscheduler.plugin.task.api.enums.Direct; @@ -39,6 +42,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; class SensitivePropertyUtilsTest { @@ -280,6 +285,91 @@ void maskErrorCommandMasksSensitiveCommandParam() { Assertions.assertNotSame(original, masked); } + @Test + void encodeForCreateEncryptsWhenEnabled() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mockEncryptionEnabled(mocked); + List encoded = SensitivePropertyUtils.encodeForCreate( + Collections.singletonList(sensitive("pwd", "Secret123"))); + Assertions.assertNotEquals("Secret123", encoded.get(0).getValue()); + Assertions.assertEquals("Secret123", PasswordUtils.decodePassword(encoded.get(0).getValue())); + } + } + + @Test + void encodeForCreateSkipsWhenDisabled() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mockEncryptionDisabled(mocked); + List encoded = SensitivePropertyUtils.encodeForCreate( + Collections.singletonList(sensitive("pwd", "Secret123"))); + Assertions.assertEquals("Secret123", encoded.get(0).getValue()); + } + } + + @Test + void mergeAndEncodeDoesNotDoubleEncryptKeepOriginal() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mockEncryptionEnabled(mocked); + String ciphertext = PasswordUtils.encodePassword("Secret123"); + List encoded = SensitivePropertyUtils.mergeAndEncode( + Collections.singletonList(sensitive("pwd", TaskConstants.SENSITIVE_DATA_MASK)), + Collections.singletonList(sensitive("pwd", ciphertext))); + Assertions.assertEquals(ciphertext, encoded.get(0).getValue()); + } + } + + @Test + void mergeAndEncodeFalseToTrueWithPlaceholderThenEncrypts() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mockEncryptionEnabled(mocked); + List encoded = SensitivePropertyUtils.mergeAndEncode( + Collections.singletonList(sensitive("pwd", TaskConstants.SENSITIVE_DATA_MASK)), + Collections.singletonList(nonSensitive("pwd", "plain"))); + Assertions.assertNotEquals("plain", encoded.get(0).getValue()); + Assertions.assertEquals("plain", PasswordUtils.decodePassword(encoded.get(0).getValue())); + Assertions.assertTrue(encoded.get(0).isSensitive()); + } + } + + @Test + void mergeAndEncodeTrueToFalseDecodesWhenCiphertextResubmitted() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mockEncryptionEnabled(mocked); + String ciphertext = PasswordUtils.encodePassword("Secret123"); + List encoded = SensitivePropertyUtils.mergeAndEncode( + Collections.singletonList(nonSensitive("pwd", ciphertext)), + Collections.singletonList(sensitive("pwd", ciphertext))); + Assertions.assertEquals("Secret123", encoded.get(0).getValue()); + Assertions.assertFalse(encoded.get(0).isSensitive()); + } + } + + @Test + void restoreStartParamsDecryptsDefinitionGlobals() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mockEncryptionEnabled(mocked); + String ciphertext = PasswordUtils.encodePassword("Secret123"); + List restored = SensitivePropertyUtils.restoreStartParams( + Collections.singletonList(sensitive("pwd", TaskConstants.SENSITIVE_DATA_MASK)), + Collections.singletonList(sensitive("pwd", ciphertext))); + Assertions.assertEquals("Secret123", restored.get(0).getValue()); + Assertions.assertTrue(restored.get(0).isSensitive()); + } + } + + private static void mockEncryptionEnabled(MockedStatic mocked) { + mocked.when(() -> PropertyUtils.getBoolean(DataSourceConstants.DATASOURCE_ENCRYPTION_ENABLE, false)) + .thenReturn(true); + mocked.when(() -> PropertyUtils.getString(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT, + DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT)) + .thenReturn(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT); + } + + private static void mockEncryptionDisabled(MockedStatic mocked) { + mocked.when(() -> PropertyUtils.getBoolean(DataSourceConstants.DATASOURCE_ENCRYPTION_ENABLE, false)) + .thenReturn(false); + } + private static Property sensitive(String prop, String value) { return Property.builder() .prop(prop) diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java index 414921a6ba98..e149d4387888 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java @@ -35,6 +35,7 @@ import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecution; import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecutionBuilder; import org.apache.dolphinscheduler.server.master.runner.WorkflowExecuteContext.WorkflowExecuteContextBuilder; +import org.apache.dolphinscheduler.server.master.utils.SensitivePropertyCryptoUtils; import org.apache.commons.collections4.CollectionUtils; @@ -125,7 +126,8 @@ private String mergeCommandParamsWithWorkflowParams(final Command command, .map(ICommandParam::getCommandParams) .orElse(null); final List globalParamsList = - GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()); + SensitivePropertyCryptoUtils.decodeSensitiveValues( + GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams())); Map finalParams = new HashMap<>(); if (CollectionUtils.isNotEmpty(globalParamsList)) { globalParamsList.forEach(globalParam -> finalParams.put(globalParam.getProp(), globalParam)); diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java index 69765d690e18..6338ba481e78 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java @@ -29,6 +29,7 @@ import org.apache.dolphinscheduler.plugin.task.api.enums.TaskTimeoutStrategy; import org.apache.dolphinscheduler.plugin.task.api.model.Property; import org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper; +import org.apache.dolphinscheduler.server.master.utils.SensitivePropertyCryptoUtils; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -81,7 +82,8 @@ public TaskExecutionContextBuilder buildTaskDefinitionRelatedInfo(final TaskDefi (int) Math.min(TimeUnit.MINUTES.toSeconds(taskDefinition.getTimeout()), Integer.MAX_VALUE)); } } - taskExecutionContext.setTaskParams(taskDefinition.getTaskParams()); + taskExecutionContext.setTaskParams( + SensitivePropertyCryptoUtils.decodeLocalParamsInTaskParams(taskDefinition.getTaskParams())); return this; } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java index 653088911514..d6627150a499 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java @@ -46,6 +46,7 @@ import org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution; import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecutionContextBuilder; import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecutionContextCreateRequest; +import org.apache.dolphinscheduler.server.master.utils.SensitivePropertyCryptoUtils; import org.apache.dolphinscheduler.service.expand.CuringParamsService; import org.apache.dolphinscheduler.service.process.ProcessService; @@ -170,7 +171,7 @@ private Map getPrepareParams(final TaskInstance taskInstance, final Project project) { final AbstractParameters baseParam = TaskPluginManager.parseTaskParameters( taskInstance.getTaskType(), - taskInstance.getTaskParams()); + SensitivePropertyCryptoUtils.decodeLocalParamsInTaskParams(taskInstance.getTaskParams())); return curingParamsService.paramParsingPreparation( taskInstance, diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java new file mode 100644 index 000000000000..c4f23d375a32 --- /dev/null +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.server.master.utils; + +import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; +import org.apache.dolphinscheduler.plugin.task.api.model.Property; +import org.apache.dolphinscheduler.plugin.task.api.utils.PropertySensitiveUtils; + +import java.util.List; + +import lombok.experimental.UtilityClass; + +/** + * Decrypt definition-time sensitive values for execution. + * API/UI still mask; Worker receives plaintext copies only. + */ +@UtilityClass +public class SensitivePropertyCryptoUtils { + + public List decodeSensitiveValues(List properties) { + return PropertySensitiveUtils.transformSensitiveValues(properties, PasswordUtils::decodePassword); + } + + public String decodeLocalParamsInTaskParams(String taskParams) { + return PropertySensitiveUtils.transformLocalParamsInTaskParams(taskParams, + SensitivePropertyCryptoUtils::decodeSensitiveValues); + } +} diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtilsTest.java b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtilsTest.java new file mode 100644 index 000000000000..87921d98083a --- /dev/null +++ b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtilsTest.java @@ -0,0 +1,76 @@ +/* + * 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.dolphinscheduler.server.master.utils; + +import org.apache.dolphinscheduler.common.utils.PropertyUtils; +import org.apache.dolphinscheduler.plugin.datasource.api.constants.DataSourceConstants; +import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; +import org.apache.dolphinscheduler.plugin.task.api.enums.DataType; +import org.apache.dolphinscheduler.plugin.task.api.enums.Direct; +import org.apache.dolphinscheduler.plugin.task.api.model.Property; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class SensitivePropertyCryptoUtilsTest { + + @Test + void decodeSensitiveValuesDecryptsWhenEnabled() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mocked.when(() -> PropertyUtils.getBoolean(DataSourceConstants.DATASOURCE_ENCRYPTION_ENABLE, false)) + .thenReturn(true); + mocked.when(() -> PropertyUtils.getString(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT, + DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT)) + .thenReturn(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT); + + String ciphertext = PasswordUtils.encodePassword("Secret123"); + List decoded = SensitivePropertyCryptoUtils.decodeSensitiveValues( + Collections.singletonList(Property.builder() + .prop("pwd") + .direct(Direct.IN) + .type(DataType.VARCHAR) + .value(ciphertext) + .sensitive(true) + .build())); + Assertions.assertEquals("Secret123", decoded.get(0).getValue()); + } + } + + @Test + void decodeLocalParamsInTaskParams() { + try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { + mocked.when(() -> PropertyUtils.getBoolean(DataSourceConstants.DATASOURCE_ENCRYPTION_ENABLE, false)) + .thenReturn(true); + mocked.when(() -> PropertyUtils.getString(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT, + DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT)) + .thenReturn(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT); + + String ciphertext = PasswordUtils.encodePassword("token-value"); + String taskParams = "{\"localParams\":[{\"prop\":\"token\",\"direct\":\"IN\",\"type\":\"VARCHAR\"," + + "\"value\":\"" + ciphertext + "\",\"sensitive\":true}]}"; + String decoded = SensitivePropertyCryptoUtils.decodeLocalParamsInTaskParams(taskParams); + Assertions.assertTrue(decoded.contains("token-value")); + Assertions.assertFalse(decoded.contains(ciphertext)); + } + } +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtils.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtils.java index 2302206d5edb..3b15d81c60ab 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtils.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtils.java @@ -17,10 +17,12 @@ package org.apache.dolphinscheduler.plugin.task.api.utils; +import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; import org.apache.dolphinscheduler.plugin.task.api.model.Property; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import java.util.Collections; import java.util.List; @@ -31,6 +33,9 @@ import lombok.experimental.UtilityClass; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + /** * Mask / merge helpers for {@link Property#isSensitive()}. * Keep-original marker is only {@code ******}; empty / null is a real empty value. @@ -85,6 +90,28 @@ public List maskSensitiveValues(List properties) { .collect(Collectors.toList()); } + /** + * Deep-copy then apply {@code transformer} to each non-empty {@code sensitive=true} value. + * Used for definition-time encode / execution-time decode without pulling crypto deps into task-api. + */ + public List transformSensitiveValues(List properties, + Function transformer) { + if (CollectionUtils.isEmpty(properties)) { + return Collections.emptyList(); + } + return properties.stream() + .map(property -> transformSensitiveValue(property, transformer)) + .collect(Collectors.toList()); + } + + public Property transformSensitiveValue(Property property, Function transformer) { + Property copied = copy(property); + if (isSensitive(copied) && StringUtils.isNotEmpty(copied.getValue())) { + copied.setValue(transformer.apply(copied.getValue())); + } + return copied; + } + public List mergeSensitiveValuePlaceholders(List submittedProperties, List existingProperties) { if (CollectionUtils.isEmpty(submittedProperties)) { @@ -157,4 +184,26 @@ private Map toPropMap(List properties) { .filter(property -> property.getProp() != null) .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); } + + /** + * Rewrite {@code localParams} inside a taskParams JSON string. + * Returns the original string when missing / unparsable. + */ + public String transformLocalParamsInTaskParams(String taskParams, + Function, List> transform) { + if (StringUtils.isEmpty(taskParams) || transform == null) { + return taskParams; + } + ObjectNode taskParamsNode = JSONUtils.parseObject(taskParams); + if (taskParamsNode == null) { + return taskParams; + } + JsonNode localParamsNode = taskParamsNode.findValue("localParams"); + if (localParamsNode == null || localParamsNode.isNull()) { + return taskParams; + } + List localParams = JSONUtils.toList(localParamsNode.toString(), Property.class); + taskParamsNode.set("localParams", JSONUtils.toJsonNode(transform.apply(localParams))); + return JSONUtils.toJsonString(taskParamsNode); + } } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtilsTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtilsTest.java index 7227d9d31cb6..f2fa90a5c807 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtilsTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/utils/PropertySensitiveUtilsTest.java @@ -145,6 +145,27 @@ void serializationRoundTripKeepsSensitiveFlag() { Assertions.assertEquals("abc", parsed.get(0).getValue()); } + @Test + void transformSensitiveValuesAppliesTransformer() { + Property original = sensitive("pwd", "Secret123"); + List transformed = PropertySensitiveUtils.transformSensitiveValues( + Collections.singletonList(original), value -> "enc:" + value); + + Assertions.assertEquals("enc:Secret123", transformed.get(0).getValue()); + Assertions.assertEquals("Secret123", original.getValue()); + } + + @Test + void transformLocalParamsInTaskParamsDecryptsSensitive() { + String taskParams = "{\"localParams\":[{\"prop\":\"token\",\"direct\":\"IN\",\"type\":\"VARCHAR\"," + + "\"value\":\"cipher\",\"sensitive\":true}],\"rawScript\":\"echo 1\"}"; + String decoded = PropertySensitiveUtils.transformLocalParamsInTaskParams(taskParams, + props -> PropertySensitiveUtils.transformSensitiveValues(props, value -> "plain")); + Assertions.assertTrue(decoded.contains("\"plain\"")); + Assertions.assertTrue(decoded.contains("rawScript")); + Assertions.assertTrue(taskParams.contains("cipher")); + } + private static Property sensitive(String prop, String value) { return Property.builder() .prop(prop) From 5454ea32bedd0bf126c6d39f3488f91f9b7f321a Mon Sep 17 00:00:00 2001 From: det101 <3656843+det101@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:20:14 +0800 Subject: [PATCH 2/7] [DSIP-105][API] Encrypt definition snapshots written by instance edits Instance edits persist task and workflow definition history, so sensitive values written there must be encoded while instance global params stay plaintext. --- .../impl/WorkflowInstanceServiceImpl.java | 15 ++- .../api/utils/SensitivePropertyUtils.java | 24 ++-- .../service/WorkflowInstanceServiceTest.java | 116 ++++++++++++++++++ 3 files changed, 134 insertions(+), 21 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java index b22dec1def5f..9ceacfa83fb6 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java @@ -413,12 +413,13 @@ public WorkflowDefinition updateWorkflowInstance(User loginUser, long projectCod } List submittedGlobalParams = GlobalParameterUtils.deserializeGlobalParameter(globalParams); + String instanceGlobalParams = globalParams; if (CollectionUtils.isNotEmpty(submittedGlobalParams)) { - globalParams = GlobalParameterUtils.serializeGlobalParameter( + instanceGlobalParams = GlobalParameterUtils.serializeGlobalParameter( SensitivePropertyUtils.merge(submittedGlobalParams, GlobalParameterUtils.deserializeGlobalParameter(workflowInstance.getGlobalParams()))); } - setWorkflowInstance(workflowInstance, scheduleTime, globalParams, timeout, timezoneId); + setWorkflowInstance(workflowInstance, scheduleTime, instanceGlobalParams, timeout, timezoneId); List taskDefinitionLogs = JSONUtils.toList(taskDefinitionJson, TaskDefinitionLog.class); if (taskDefinitionLogs.isEmpty()) { log.warn("Parameter taskDefinitionJson is empty"); @@ -449,7 +450,7 @@ public WorkflowDefinition updateWorkflowInstance(User loginUser, long projectCod ? null : existingTaskParamsMap.get(submitted.getCode() + "_" + submitted.getVersion()); submitted.setTaskParams(SensitivePropertyUtils.mergeLocalParams( - submitted.getTaskParams(), existingTaskParams, false)); + submitted.getTaskParams(), existingTaskParams)); } taskDatasourcePermissionChecker.checkPermission(loginUser, taskDefinitionLogs); taskSubWorkflowPermissionChecker.checkPermission(loginUser, taskDefinitionLogs); @@ -466,8 +467,14 @@ public WorkflowDefinition updateWorkflowInstance(User loginUser, long projectCod // check workflow json is valid (throws ServiceException on validation failures) workflowDefinitionService.checkWorkflowNodeList(taskRelationJson, taskDefinitionLogs); + String definitionGlobalParams = globalParams; + if (CollectionUtils.isNotEmpty(submittedGlobalParams)) { + definitionGlobalParams = GlobalParameterUtils.serializeGlobalParameter( + SensitivePropertyUtils.mergeAndEncode(submittedGlobalParams, + GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()))); + } workflowDefinition.set(projectCode, workflowDefinition.getName(), workflowDefinition.getDescription(), - globalParams, locations, timeout); + definitionGlobalParams, locations, timeout); workflowDefinition.setUpdateTime(new Date()); int insertVersion = processService.saveWorkflowDefine(loginUser, workflowDefinition, syncDefine, Boolean.FALSE); if (insertVersion == 0) { diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java index 83069b0f0b01..8b96e7eab501 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java @@ -91,7 +91,8 @@ public List encodeForCreate(List properties) { * Update definition params: restore DB value for keep-original {@code ******}, then encode. * Empty / null is a real empty value. {@code false→true} + {@code ******} is allowed; * {@code true→false} + {@code ******} is rejected. - * Runtime instance updates should call {@link #merge} only (no encode). + * Instance {@code global_params} stay plaintext via {@link #merge}. Copies written to a workflow + * definition must use this method against the definition's stored values, not the instance plaintext. */ public List mergeAndEncode(List submittedProperties, List existingProperties) { List merged = merge(submittedProperties, existingProperties); @@ -161,30 +162,19 @@ public List decodeSensitiveValues(List properties) { return PropertySensitiveUtils.transformSensitiveValues(properties, PasswordUtils::decodePassword); } - public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { - return mergeLocalParams(submittedTaskParams, existingTaskParams, true); - } - /** - * @param encodeForDefinition when true, encode after merge (workflow/task definition persist); - * when false, merge only (runtime instance edit). + * Merge task {@code localParams} and encode sensitive values for definition persist. + * Instance edits also persist task-definition snapshots, so this always encodes. */ - public String mergeLocalParams(String submittedTaskParams, String existingTaskParams, - boolean encodeForDefinition) { + public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { return rewriteLocalParams(submittedTaskParams, submitted -> { if (StringUtils.isEmpty(existingTaskParams)) { - return encodeForDefinition ? encodeForCreate(submitted) : requireNoPlaceholderAndReturn(submitted); + return encodeForCreate(submitted); } - List existing = getLocalParams(existingTaskParams); - return encodeForDefinition ? mergeAndEncode(submitted, existing) : merge(submitted, existing); + return mergeAndEncode(submitted, getLocalParams(existingTaskParams)); }); } - private List requireNoPlaceholderAndReturn(List submitted) { - requireNoPlaceholder(submitted); - return submitted; - } - /** * Encode sensitive values that are new plaintext for definition persist. * Keep-original from an already-sensitive existing value is written as-is (no re-encode). diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java index 97bf50e73100..7187fcee7859 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java @@ -49,6 +49,7 @@ import org.apache.dolphinscheduler.common.model.TaskNodeRelation; import org.apache.dolphinscheduler.common.utils.DateUtils; import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.common.utils.PropertyUtils; import org.apache.dolphinscheduler.dao.AlertDao; import org.apache.dolphinscheduler.dao.entity.DependentResultTaskInstanceContext; import org.apache.dolphinscheduler.dao.entity.Project; @@ -74,6 +75,8 @@ import org.apache.dolphinscheduler.dao.repository.WorkflowInstanceDao; import org.apache.dolphinscheduler.dao.repository.WorkflowInstanceMapDao; import org.apache.dolphinscheduler.extract.master.command.RunWorkflowCommandParam; +import org.apache.dolphinscheduler.plugin.datasource.api.constants.DataSourceConstants; +import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.plugin.task.api.enums.DataType; @@ -81,6 +84,7 @@ import org.apache.dolphinscheduler.plugin.task.api.enums.Direct; import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus; import org.apache.dolphinscheduler.plugin.task.api.model.Property; +import org.apache.dolphinscheduler.plugin.task.api.utils.GlobalParameterUtils; import org.apache.dolphinscheduler.service.expand.CuringParamsService; import org.apache.dolphinscheduler.service.model.TaskNode; import org.apache.dolphinscheduler.service.process.ProcessService; @@ -806,6 +810,118 @@ public void testUpdateWorkflowInstancePersistsPlaintextSensitiveGlobalParams() { Assertions.assertSame(updated, persisted.getValue()); } + @Test + public void testUpdateWorkflowInstanceEncryptsDefinitionSnapshotsForBothSyncDefine() { + try ( + MockedStatic propertyUtils = Mockito.mockStatic(PropertyUtils.class); + MockedStatic taskPluginManager = Mockito.mockStatic(TaskPluginManager.class)) { + propertyUtils.when(() -> PropertyUtils.getBoolean(DataSourceConstants.DATASOURCE_ENCRYPTION_ENABLE, false)) + .thenReturn(true); + propertyUtils.when(() -> PropertyUtils.getString(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT, + DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT)) + .thenReturn(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT); + taskPluginManager.when(() -> TaskPluginManager.checkTaskParameters(any(), any())).thenReturn(true); + when(curingGlobalParamsService.curingGlobalParams(any(), any(), any(), any(), any(), any())) + .thenAnswer(invocation -> GlobalParameterUtils.serializeGlobalParameter(invocation.getArgument(2))); + + String keptGlobalCipher = PasswordUtils.encodePassword("kept-secret"); + String keptLocalCipher = PasswordUtils.encodePassword("kept-local"); + assertInstanceEditEncryptsDefinitionSnapshot(Boolean.TRUE, keptGlobalCipher, keptLocalCipher); + assertInstanceEditEncryptsDefinitionSnapshot(Boolean.FALSE, keptGlobalCipher, keptLocalCipher); + } + } + + private void assertInstanceEditEncryptsDefinitionSnapshot(Boolean syncDefine, String keptGlobalCipher, + String keptLocalCipher) { + Mockito.clearInvocations(processService); + long projectCode = 1L; + User loginUser = getAdminUser(); + WorkflowInstance workflowInstance = getProcessInstance(); + workflowInstance.setProjectCode(projectCode); + workflowInstance.setState(WorkflowExecutionStatus.SUCCESS); + workflowInstance.setTimeout(3000); + workflowInstance.setCommandType(CommandType.STOP); + workflowInstance.setWorkflowDefinitionCode(46L); + workflowInstance.setWorkflowDefinitionVersion(1); + workflowInstance.setGlobalParams( + "[{\"prop\":\"kept\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"kept-secret\",\"sensitive\":true}]"); + WorkflowDefinition workflowDefinition = getProcessDefinition(); + workflowDefinition.setProjectCode(projectCode); + workflowDefinition.setGlobalParams( + "[{\"prop\":\"kept\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"" + keptGlobalCipher + + "\",\"sensitive\":true}]"); + + doNothing().when(projectService).checkHasProjectWritePermissionThrowException(loginUser, projectCode); + when(processService.findWorkflowInstanceDetailById(1)).thenReturn(Optional.of(workflowInstance)); + when(workflowDefinitionDao.queryByCode(46L)).thenReturn(Optional.of(workflowDefinition)); + when(workflowInstanceDao.updateById(workflowInstance)).thenReturn(true); + Mockito.doNothing().when(workflowDefinitionService).checkWorkflowNodeList(any(), any()); + Mockito.doNothing().when(taskDatasourcePermissionChecker).checkPermission(any(), any()); + when(processService.saveTaskRelation(any(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyInt(), any(), + any(), any())).thenReturn(Constants.EXIT_CODE_SUCCESS); + + TaskDefinitionLog existingTaskLog = new TaskDefinitionLog(); + existingTaskLog.setCode(4254862762304L); + existingTaskLog.setVersion(1); + existingTaskLog.setTaskParams( + "{\"localParams\":[{\"prop\":\"keptLocal\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"" + + keptLocalCipher + "\",\"sensitive\":true}],\"rawScript\":\"echo 1\"}"); + when(taskDefinitionLogMapper.queryByTaskDefinitions(any())) + .thenReturn(Collections.singletonList(existingTaskLog)); + + String submittedGlobalParams = + "[{\"prop\":\"kept\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"******\",\"sensitive\":true}," + + "{\"prop\":\"pwd\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"new-global\",\"sensitive\":true}]"; + String submittedTaskDefinitionJson = + "[{\"code\":4254862762304,\"name\":\"test1\",\"version\":1,\"description\":\"\",\"delayTime\":0," + + "\"taskType\":\"SHELL\",\"taskParams\":{\"resourceList\":[],\"localParams\":[" + + "{\"prop\":\"keptLocal\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"******\",\"sensitive\":true}," + + "{\"prop\":\"apiKey\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"new-local\",\"sensitive\":true}" + + "],\"rawScript\":\"echo 1\"},\"flag\":\"YES\",\"taskPriority\":\"MEDIUM\",\"workerGroup\":\"default\"," + + "\"failRetryTimes\":0,\"failRetryInterval\":1,\"timeoutFlag\":\"CLOSE\",\"timeoutNotifyStrategy\":null," + + "\"timeout\":0,\"environmentCode\":-1}]"; + + ArgumentCaptor savedTaskLogs = ArgumentCaptor.forClass(List.class); + when(processService.saveTaskDefine(any(), Mockito.anyLong(), savedTaskLogs.capture(), eq(syncDefine))) + .thenReturn(1); + when(processService.saveWorkflowDefine(any(), eq(workflowDefinition), eq(syncDefine), eq(Boolean.FALSE))) + .thenReturn(1); + + workflowInstanceService.updateWorkflowInstance(loginUser, projectCode, 1, + taskRelationJson, submittedTaskDefinitionJson, "2020-02-21 00:00:00", syncDefine, + submittedGlobalParams, "", 0); + + List instanceGlobals = + GlobalParameterUtils.deserializeGlobalParameter(workflowInstance.getGlobalParams()); + Assertions.assertEquals("kept-secret", findProperty(instanceGlobals, "kept").getValue()); + Assertions.assertEquals("new-global", findProperty(instanceGlobals, "pwd").getValue()); + + List definitionGlobals = + GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()); + Assertions.assertEquals(keptGlobalCipher, findProperty(definitionGlobals, "kept").getValue()); + Property encodedGlobal = findProperty(definitionGlobals, "pwd"); + Assertions.assertNotEquals("new-global", encodedGlobal.getValue()); + Assertions.assertEquals("new-global", PasswordUtils.decodePassword(encodedGlobal.getValue())); + Assertions.assertFalse(workflowDefinition.getGlobalParams().contains("kept-secret")); + + TaskDefinitionLog savedTask = ((List) savedTaskLogs.getValue()).get(0); + List localParams = JSONUtils.toList( + JSONUtils.getNodeString(savedTask.getTaskParams(), "localParams"), Property.class); + Assertions.assertEquals(keptLocalCipher, findProperty(localParams, "keptLocal").getValue()); + Property encodedLocal = findProperty(localParams, "apiKey"); + Assertions.assertNotEquals("new-local", encodedLocal.getValue()); + Assertions.assertEquals("new-local", PasswordUtils.decodePassword(encodedLocal.getValue())); + verify(processService).saveTaskDefine(eq(loginUser), eq(projectCode), any(), eq(syncDefine)); + verify(processService).saveWorkflowDefine(any(), eq(workflowDefinition), eq(syncDefine), eq(Boolean.FALSE)); + } + + private static Property findProperty(List properties, String prop) { + return properties.stream() + .filter(property -> prop.equals(property.getProp())) + .findFirst() + .orElseThrow(() -> new AssertionError(prop)); + } + @Test public void testQueryParentInstanceBySubId() { long projectCode = 1L; From a074c87a6049d4a5fae076476999d520209c4dff Mon Sep 17 00:00:00 2001 From: det101 <3656843+det101@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:54:26 +0800 Subject: [PATCH 3/7] [Chore] Trigger CI rerun for flaky E2E From 4ce442acc20210670dba160fa7180e6b6e4380f9 Mon Sep 17 00:00:00 2001 From: det101 <3656843+det101@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:54:24 +0800 Subject: [PATCH 4/7] [DSIP-105][API] Encode definition snapshot from instance-resolved globals Address review feedback: when editing a workflow instance, build the encrypted definition snapshot from already-resolved instance globals instead of merging ****** against the latest definition again. Keep instance global_params plaintext and encode only at the definition persist boundary. Add coverage for instance override differing from definition for both syncDefine values. --- .../impl/WorkflowDefinitionServiceImpl.java | 69 ++++++-- .../impl/WorkflowInstanceServiceImpl.java | 39 ++-- .../api/utils/SensitivePropertyUtils.java | 166 ++++++++++-------- .../BackfillWorkflowRequestTransformer.java | 4 +- .../TriggerWorkflowRequestTransformer.java | 4 +- .../service/WorkflowInstanceServiceTest.java | 62 +++++++ .../api/utils/SensitivePropertyUtilsTest.java | 39 ++-- .../handler/AbstractCommandHandler.java | 4 + .../handler/RunWorkflowCommandHandler.java | 6 +- .../engine/graph/WorkflowGraphFactory.java | 2 + .../TaskExecutionContextBuilder.java | 1 + .../utils/SensitivePropertyCryptoUtils.java | 21 ++- 12 files changed, 297 insertions(+), 120 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java index b95a5cba8908..3cc5f95854a4 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowDefinitionServiceImpl.java @@ -265,12 +265,7 @@ public WorkflowDefinition createWorkflowDefinition(User loginUser, List taskDefinitionLogs = generateTaskDefinitionList(taskDefinitionJson); List taskRelationList = generateTaskRelationList(taskRelationJson, taskDefinitionLogs); List submittedGlobalParams = GlobalParameterUtils.deserializeGlobalParameter(globalParams); - if (CollectionUtils.isNotEmpty(submittedGlobalParams)) { - globalParams = GlobalParameterUtils.serializeGlobalParameter( - SensitivePropertyUtils.encodeForCreate(submittedGlobalParams)); - } else { - SensitivePropertyUtils.requireNoPlaceholder(submittedGlobalParams); - } + SensitivePropertyUtils.requireNoPlaceholder(submittedGlobalParams); for (TaskDefinitionLog taskDefinitionLog : CollectionUtils.emptyIfNull(taskDefinitionLogs)) { taskDefinitionLog.setTaskParams( SensitivePropertyUtils.mergeLocalParams(taskDefinitionLog.getTaskParams(), null)); @@ -282,15 +277,44 @@ public WorkflowDefinition createWorkflowDefinition(User loginUser, globalParams, locations, timeout, loginUser.getId()); workflowDefinition.setExecutionType(executionType); - return createDagDefine(loginUser, taskRelationList, workflowDefinition, taskDefinitionLogs); + // encodeSensitiveParams=true: plaintext through checks, encode inside createDagDefine before save*. + return createDagDefine(loginUser, taskRelationList, workflowDefinition, taskDefinitionLogs, true); + } + + /** Persist-boundary: encode task localParams immediately before {@code saveTaskDefine}. */ + private void encodeTaskParamsForPersist(List taskDefinitionLogs) { + for (TaskDefinitionLog taskDefinitionLog : CollectionUtils.emptyIfNull(taskDefinitionLogs)) { + taskDefinitionLog.setTaskParams( + SensitivePropertyUtils.encodeLocalParamsInTaskParams(taskDefinitionLog.getTaskParams())); + } + } + + /** Persist-boundary: encode workflow globalParams immediately before {@code saveWorkflowDefine}. */ + private void encodeGlobalParamsForPersist(WorkflowDefinition workflowDefinition) { + if (workflowDefinition != null && StringUtils.isNotEmpty(workflowDefinition.getGlobalParams())) { + workflowDefinition.setGlobalParams( + SensitivePropertyUtils.encodeGlobalParams(workflowDefinition.getGlobalParams())); + } } protected WorkflowDefinition createDagDefine(User loginUser, List taskRelationList, WorkflowDefinition workflowDefinition, List taskDefinitionLogs) { + // Copy / import paths already persist ciphertext — do not encode again. + return createDagDefine(loginUser, taskRelationList, workflowDefinition, taskDefinitionLogs, false); + } + + protected WorkflowDefinition createDagDefine(User loginUser, + List taskRelationList, + WorkflowDefinition workflowDefinition, + List taskDefinitionLogs, + boolean encodeSensitiveParams) { taskDatasourcePermissionChecker.checkPermission(loginUser, taskDefinitionLogs); taskSubWorkflowPermissionChecker.checkPermission(loginUser, taskDefinitionLogs); + if (encodeSensitiveParams) { + encodeTaskParamsForPersist(taskDefinitionLogs); + } int saveTaskResult = processService.saveTaskDefine(loginUser, workflowDefinition.getProjectCode(), taskDefinitionLogs, Boolean.TRUE); if (saveTaskResult == Constants.EXIT_CODE_SUCCESS) { @@ -300,6 +324,9 @@ protected WorkflowDefinition createDagDefine(User loginUser, log.error("Save task definition error."); throw new ServiceException(Status.CREATE_TASK_DEFINITION_ERROR); } + if (encodeSensitiveParams) { + encodeGlobalParamsForPersist(workflowDefinition); + } int insertVersion = processService.saveWorkflowDefine(loginUser, workflowDefinition, Boolean.TRUE, Boolean.TRUE); if (insertVersion == 0) { @@ -674,9 +701,12 @@ public WorkflowDefinition updateWorkflowDefinition(User loginUser, List submittedGlobalParams = GlobalParameterUtils.deserializeGlobalParameter(globalParams); if (CollectionUtils.isNotEmpty(submittedGlobalParams)) { + List existingRaw = + GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()); + List merged = SensitivePropertyUtils.merge(submittedGlobalParams, + SensitivePropertyUtils.decodeAfterLoad(existingRaw)); globalParams = GlobalParameterUtils.serializeGlobalParameter( - SensitivePropertyUtils.mergeAndEncode(submittedGlobalParams, - GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()))); + SensitivePropertyUtils.normalizeResubmittedCiphertext(merged, existingRaw)); } List versionKeys = new ArrayList<>(); for (TaskDefinitionLog taskDefinitionLog : CollectionUtils.emptyIfNull(taskDefinitionLogs)) { @@ -696,6 +726,7 @@ public WorkflowDefinition updateWorkflowDefinition(User loginUser, String existingTaskParams = submitted.getCode() <= 0 || submitted.getVersion() <= 0 ? null : existingTaskParamsMap.get(submitted.getCode() + "_" + submitted.getVersion()); + // Keep plaintext; encode inside updateDagDefine immediately before save*. submitted.setTaskParams(SensitivePropertyUtils.mergeLocalParams( submitted.getTaskParams(), existingTaskParams)); } @@ -704,7 +735,7 @@ public WorkflowDefinition updateWorkflowDefinition(User loginUser, workflowDefinition.set(projectCode, name, description, globalParams, locations, timeout); workflowDefinition.setExecutionType(executionType); return updateDagDefine(loginUser, taskRelationList, workflowDefinition, workflowDefinitionDeepCopy, - taskDefinitionLogs); + taskDefinitionLogs, true); } /** @@ -744,8 +775,22 @@ protected WorkflowDefinition updateDagDefine(User loginUser, WorkflowDefinition workflowDefinition, WorkflowDefinition workflowDefinitionDeepCopy, List taskDefinitionLogs) { + // Move path already has ciphertext — do not encode again. + return updateDagDefine(loginUser, taskRelationList, workflowDefinition, workflowDefinitionDeepCopy, + taskDefinitionLogs, false); + } + + protected WorkflowDefinition updateDagDefine(User loginUser, + List taskRelationList, + WorkflowDefinition workflowDefinition, + WorkflowDefinition workflowDefinitionDeepCopy, + List taskDefinitionLogs, + boolean encodeSensitiveParams) { taskDatasourcePermissionChecker.checkPermission(loginUser, taskDefinitionLogs); taskSubWorkflowPermissionChecker.checkPermission(loginUser, taskDefinitionLogs); + if (encodeSensitiveParams) { + encodeTaskParamsForPersist(taskDefinitionLogs); + } int saveTaskResult = processService.saveTaskDefine(loginUser, workflowDefinition.getProjectCode(), taskDefinitionLogs, Boolean.TRUE); if (saveTaskResult == Constants.EXIT_CODE_SUCCESS) { @@ -756,6 +801,10 @@ protected WorkflowDefinition updateDagDefine(User loginUser, workflowDefinition.getProjectCode(), workflowDefinition.getCode()); throw new ServiceException(Status.UPDATE_TASK_DEFINITION_ERROR); } + // Encode globals before equals/save so comparison matches DB ciphertext (deepCopy). + if (encodeSensitiveParams) { + encodeGlobalParamsForPersist(workflowDefinition); + } boolean isChange = false; if (workflowDefinition.equals(workflowDefinitionDeepCopy) && saveTaskResult == Constants.EXIT_CODE_SUCCESS) { List workflowTaskRelationLogList = workflowTaskRelationLogMapper diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java index 9ceacfa83fb6..2ec6d15ec8b0 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/WorkflowInstanceServiceImpl.java @@ -449,33 +449,44 @@ public WorkflowDefinition updateWorkflowInstance(User loginUser, long projectCod String existingTaskParams = submitted.getCode() <= 0 || submitted.getVersion() <= 0 ? null : existingTaskParamsMap.get(submitted.getCode() + "_" + submitted.getVersion()); + // Keep plaintext through permission / DAG checks; encode only at each save* below. submitted.setTaskParams(SensitivePropertyUtils.mergeLocalParams( submitted.getTaskParams(), existingTaskParams)); } taskDatasourcePermissionChecker.checkPermission(loginUser, taskDefinitionLogs); taskSubWorkflowPermissionChecker.checkPermission(loginUser, taskDefinitionLogs); + List taskRelationList = + JSONUtils.toList(taskRelationJson, WorkflowTaskRelationLog.class); + // Validate on plaintext before any definition persist. + workflowDefinitionService.checkWorkflowNodeList(taskRelationJson, taskDefinitionLogs); + + WorkflowDefinition workflowDefinition = + workflowDefinitionDao.queryByCode(workflowInstance.getWorkflowDefinitionCode()).orElse(null); + // Definition snapshot uses instance-resolved globals (****** already merged); do not merge again + // against the latest definition, or syncDefine would diverge when instance overrode a secret. + String definitionGlobalParams = CollectionUtils.isNotEmpty(submittedGlobalParams) + ? instanceGlobalParams + : globalParams; + workflowDefinition.set(projectCode, workflowDefinition.getName(), workflowDefinition.getDescription(), + definitionGlobalParams, locations, timeout); + workflowDefinition.setUpdateTime(new Date()); + + // Persist boundary: encode definition snapshots immediately before each DB write. + // Instance global_params stay plaintext; only the definition copy is encoded here. + for (TaskDefinitionLog submitted : taskDefinitionLogs) { + submitted.setTaskParams( + SensitivePropertyUtils.encodeLocalParamsInTaskParams(submitted.getTaskParams())); + } int saveTaskResult = processService.saveTaskDefine(loginUser, projectCode, taskDefinitionLogs, syncDefine); if (saveTaskResult == Constants.DEFINITION_FAILURE) { log.error("Update task definition error, projectCode:{}, workflowInstanceId:{}", projectCode, workflowInstanceId); throw new ServiceException(Status.UPDATE_TASK_DEFINITION_ERROR); } - WorkflowDefinition workflowDefinition = - workflowDefinitionDao.queryByCode(workflowInstance.getWorkflowDefinitionCode()).orElse(null); - List taskRelationList = - JSONUtils.toList(taskRelationJson, WorkflowTaskRelationLog.class); - // check workflow json is valid (throws ServiceException on validation failures) - workflowDefinitionService.checkWorkflowNodeList(taskRelationJson, taskDefinitionLogs); - - String definitionGlobalParams = globalParams; if (CollectionUtils.isNotEmpty(submittedGlobalParams)) { - definitionGlobalParams = GlobalParameterUtils.serializeGlobalParameter( - SensitivePropertyUtils.mergeAndEncode(submittedGlobalParams, - GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()))); + workflowDefinition.setGlobalParams( + SensitivePropertyUtils.encodeGlobalParams(workflowDefinition.getGlobalParams())); } - workflowDefinition.set(projectCode, workflowDefinition.getName(), workflowDefinition.getDescription(), - definitionGlobalParams, locations, timeout); - workflowDefinition.setUpdateTime(new Date()); int insertVersion = processService.saveWorkflowDefine(loginUser, workflowDefinition, syncDefine, Boolean.FALSE); if (insertVersion == 0) { log.error("Update workflow definition error, projectCode:{}, workflowDefinitionName:{}.", projectCode, diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java index 8b96e7eab501..575879383c2d 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java @@ -63,8 +63,14 @@ /** * HTTP-layer helpers for {@link Property#isSensitive()}. - * Definition write path: merge keep-original then encode with {@link PasswordUtils} when enabled. - * Query masks values as {@code ******}. Start / execution use decrypt copies. + *

    + * Crypto boundary (definition params only): + *

      + *
    • {@link #encodeForPersist} / {@link #encodeLocalParamsInTaskParams} — right before DB write
    • + *
    • {@link #decodeAfterLoad} / {@link #decodeLocalParamsInTaskParams} — right after DB read
    • + *
    + * Business logic (merge / curing / permission) stays on plaintext. Instance {@code global_params} + * stay plaintext (no encode). Query responses use {@link #mask}. */ @UtilityClass public class SensitivePropertyUtils { @@ -80,28 +86,11 @@ public void requireNoPlaceholder(List properties) { } /** - * Create definition params: reject {@code ******}, then encode sensitive plaintext. - */ - public List encodeForCreate(List properties) { - requireNoPlaceholder(properties); - return encodeNewSensitivePlaintext(properties, Collections.emptyList()); - } - - /** - * Update definition params: restore DB value for keep-original {@code ******}, then encode. + * Update: restore DB value for keep-original {@code ******} without encoding. + * {@code existingProperties} must already be plaintext ({@link #decodeAfterLoad}). * Empty / null is a real empty value. {@code false→true} + {@code ******} is allowed; * {@code true→false} + {@code ******} is rejected. - * Instance {@code global_params} stay plaintext via {@link #merge}. Copies written to a workflow - * definition must use this method against the definition's stored values, not the instance plaintext. - */ - public List mergeAndEncode(List submittedProperties, List existingProperties) { - List merged = merge(submittedProperties, existingProperties); - return encodeNewSensitivePlaintext(merged, existingProperties); - } - - /** - * Update: restore DB value for keep-original {@code ******} without encoding. - * Used for runtime instance global_params (plaintext materialization). + * Instance {@code global_params} use this and stay plaintext. */ public List merge(List submittedProperties, List existingProperties) { if (CollectionUtils.isEmpty(submittedProperties)) { @@ -118,8 +107,53 @@ public List merge(List submittedProperties, List e } /** - * Build start/command params before persist. - * Decrypts definition globals first, then: + * Persist boundary: encode every non-empty {@code sensitive=true} value. + * Call immediately before writing definition rows. Idempotent when encryption is off. + */ + public List encodeForPersist(List properties) { + return PropertySensitiveUtils.transformSensitiveValues(properties, PasswordUtils::encodePassword); + } + + /** + * Load boundary: decode every non-empty {@code sensitive=true} value. + * Call immediately after reading definition rows into business logic. + */ + public List decodeAfterLoad(List properties) { + return PropertySensitiveUtils.transformSensitiveValues(properties, PasswordUtils::decodePassword); + } + + /** + * {@code true→false}: if a non-UI client re-sent the raw DB ciphertext, restore plaintext. + * {@code existingRawFromDb} is still ciphertext (before {@link #decodeAfterLoad}). + */ + public List normalizeResubmittedCiphertext(List mergedPlaintext, + List existingRawFromDb) { + if (CollectionUtils.isEmpty(mergedPlaintext) || CollectionUtils.isEmpty(existingRawFromDb)) { + return mergedPlaintext; + } + Map existingRawMap = existingRawFromDb.stream() + .filter(Objects::nonNull) + .filter(property -> property.getProp() != null) + .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); + List normalized = new ArrayList<>(mergedPlaintext.size()); + for (Property property : mergedPlaintext) { + Property copy = PropertySensitiveUtils.copy(property); + if (!PropertySensitiveUtils.isSensitive(copy)) { + Property existingRaw = existingRawMap.get(copy.getProp()); + if (existingRaw != null && existingRaw.isSensitive() + && StringUtils.isNotEmpty(copy.getValue()) + && Objects.equals(copy.getValue(), existingRaw.getValue())) { + copy.setValue(PasswordUtils.decodePassword(copy.getValue())); + } + } + normalized.add(copy); + } + return normalized; + } + + /** + * Build start/command params. {@code globalParams} must already be plaintext + * ({@link #decodeAfterLoad} at the load boundary). *
      *
    • Same name as a workflow global: inherit all global attributes, only override {@code value} * ({@code ******} keeps the global plaintext).
    • @@ -130,8 +164,7 @@ public List restoreStartParams(List startParams, List decryptedGlobals = decodeSensitiveValues(globalParams); - Map globals = CollectionUtils.emptyIfNull(decryptedGlobals).stream() + Map globals = CollectionUtils.emptyIfNull(globalParams).stream() .filter(Objects::nonNull) .filter(property -> property.getProp() != null) .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); @@ -158,66 +191,55 @@ public List restoreStartParams(List startParams, List decodeSensitiveValues(List properties) { - return PropertySensitiveUtils.transformSensitiveValues(properties, PasswordUtils::decodePassword); + return decodeAfterLoad(properties); } /** - * Merge task {@code localParams} and encode sensitive values for definition persist. - * Instance edits also persist task-definition snapshots, so this always encodes. + * Merge task {@code localParams} to plaintext for business logic. + * {@code existingTaskParams} may still be DB ciphertext; existing side is decoded here. + * Call {@link #encodeLocalParamsInTaskParams} immediately before definition persist. */ public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { return rewriteLocalParams(submittedTaskParams, submitted -> { if (StringUtils.isEmpty(existingTaskParams)) { - return encodeForCreate(submitted); + requireNoPlaceholder(submitted); + return PropertySensitiveUtils.copy(submitted); } - return mergeAndEncode(submitted, getLocalParams(existingTaskParams)); + List existingRaw = getLocalParams(existingTaskParams); + List existingPlain = decodeAfterLoad(existingRaw); + List merged = merge(submitted, existingPlain); + return normalizeResubmittedCiphertext(merged, existingRaw); }); } - /** - * Encode sensitive values that are new plaintext for definition persist. - * Keep-original from an already-sensitive existing value is written as-is (no re-encode). - * {@code false→true} keep-original merges non-sensitive plaintext and then encodes. - */ - private List encodeNewSensitivePlaintext(List mergedProperties, - List existingProperties) { - if (CollectionUtils.isEmpty(mergedProperties)) { - return mergedProperties; + /** Persist boundary for task definition {@code taskParams.localParams}. */ + public String encodeLocalParamsInTaskParams(String taskParams) { + return rewriteLocalParams(taskParams, SensitivePropertyUtils::encodeForPersist); + } + + /** Load boundary for task definition / task instance {@code taskParams.localParams}. */ + public String decodeLocalParamsInTaskParams(String taskParams) { + return rewriteLocalParams(taskParams, SensitivePropertyUtils::decodeAfterLoad); + } + + /** Persist boundary helper for workflow definition {@code globalParams} JSON. */ + public String encodeGlobalParams(String globalParams) { + List properties = GlobalParameterUtils.deserializeGlobalParameter(globalParams); + if (CollectionUtils.isEmpty(properties)) { + return globalParams; } - Map existingMap = CollectionUtils.emptyIfNull(existingProperties).stream() - .filter(Objects::nonNull) - .filter(property -> property.getProp() != null) - .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); - List encoded = new ArrayList<>(mergedProperties.size()); - for (Property property : mergedProperties) { - Property copy = PropertySensitiveUtils.copy(property); - if (!PropertySensitiveUtils.isSensitive(copy)) { - Property existing = existingMap.get(copy.getProp()); - // true→false: if a non-UI client re-sent ciphertext, decode back to plaintext. - if (existing != null && existing.isSensitive() - && StringUtils.isNotEmpty(copy.getValue()) - && Objects.equals(copy.getValue(), existing.getValue())) { - copy.setValue(PasswordUtils.decodePassword(copy.getValue())); - } - encoded.add(copy); - continue; - } - if (StringUtils.isEmpty(copy.getValue())) { - encoded.add(copy); - continue; - } - Property existing = existingMap.get(copy.getProp()); - if (existing != null && existing.isSensitive() - && Objects.equals(copy.getValue(), existing.getValue())) { - // true→true keep-original (or identical resubmit): already persisted form. - encoded.add(copy); - continue; - } - copy.setValue(PasswordUtils.encodePassword(copy.getValue())); - encoded.add(copy); + return GlobalParameterUtils.serializeGlobalParameter(encodeForPersist(properties)); + } + + /** Load boundary helper for workflow definition {@code globalParams} JSON. */ + public String decodeGlobalParams(String globalParams) { + List properties = GlobalParameterUtils.deserializeGlobalParameter(globalParams); + if (CollectionUtils.isEmpty(properties)) { + return globalParams; } - return encoded; + return GlobalParameterUtils.serializeGlobalParameter(decodeAfterLoad(properties)); } /** diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowRequestTransformer.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowRequestTransformer.java index b66f6b2a00ef..6b379ed49ccb 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowRequestTransformer.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowRequestTransformer.java @@ -77,7 +77,9 @@ public BackfillWorkflowDTO transform(WorkflowBackFillRequest workflowBackFillReq .environmentCode(workflowBackFillRequest.getEnvironmentCode()) .startParamList(SensitivePropertyUtils.restoreStartParams( PropertyUtils.startParamsTransformPropertyList(workflowBackFillRequest.getStartParamList()), - GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()))) + SensitivePropertyUtils.decodeAfterLoad( + GlobalParameterUtils.deserializeGlobalParameter( + workflowDefinition.getGlobalParams())))) .dryRun(workflowBackFillRequest.getDryRun()) .triggerCode(CodeGenerateUtils.genCode()) .backfillParams(backfillParams) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/TriggerWorkflowRequestTransformer.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/TriggerWorkflowRequestTransformer.java index 748756c73396..c38cb38520a8 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/TriggerWorkflowRequestTransformer.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/TriggerWorkflowRequestTransformer.java @@ -60,7 +60,9 @@ public TriggerWorkflowDTO transform(WorkflowTriggerRequest workflowTriggerReques .environmentCode(workflowTriggerRequest.getEnvironmentCode()) .startParamList(SensitivePropertyUtils.restoreStartParams( PropertyUtils.startParamsTransformPropertyList(workflowTriggerRequest.getStartParamList()), - GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()))) + SensitivePropertyUtils.decodeAfterLoad( + GlobalParameterUtils.deserializeGlobalParameter( + workflowDefinition.getGlobalParams())))) .dryRun(workflowTriggerRequest.getDryRun()) .build(); triggerWorkflowDTO.setWorkflowDefinition(workflowDefinition); diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java index 7187fcee7859..d3a5e12f8d16 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/WorkflowInstanceServiceTest.java @@ -828,6 +828,8 @@ public void testUpdateWorkflowInstanceEncryptsDefinitionSnapshotsForBothSyncDefi String keptLocalCipher = PasswordUtils.encodePassword("kept-local"); assertInstanceEditEncryptsDefinitionSnapshot(Boolean.TRUE, keptGlobalCipher, keptLocalCipher); assertInstanceEditEncryptsDefinitionSnapshot(Boolean.FALSE, keptGlobalCipher, keptLocalCipher); + assertInstanceEditUsesInstanceOverrideForDefinitionSnapshot(Boolean.TRUE); + assertInstanceEditUsesInstanceOverrideForDefinitionSnapshot(Boolean.FALSE); } } @@ -915,6 +917,66 @@ private void assertInstanceEditEncryptsDefinitionSnapshot(Boolean syncDefine, St verify(processService).saveWorkflowDefine(any(), eq(workflowDefinition), eq(syncDefine), eq(Boolean.FALSE)); } + /** + * Definition has secret A, instance was started with override B. Unchanged ****** must keep B on the + * instance and encode B (not A) into the definition snapshot for both syncDefine values. + */ + private void assertInstanceEditUsesInstanceOverrideForDefinitionSnapshot(Boolean syncDefine) { + Mockito.clearInvocations(processService); + long projectCode = 1L; + User loginUser = getAdminUser(); + String definitionCipher = PasswordUtils.encodePassword("definition-secret-A"); + String instancePlain = "instance-secret-B"; + + WorkflowInstance workflowInstance = getProcessInstance(); + workflowInstance.setProjectCode(projectCode); + workflowInstance.setState(WorkflowExecutionStatus.SUCCESS); + workflowInstance.setTimeout(3000); + workflowInstance.setCommandType(CommandType.STOP); + workflowInstance.setWorkflowDefinitionCode(46L); + workflowInstance.setWorkflowDefinitionVersion(1); + workflowInstance.setGlobalParams( + "[{\"prop\":\"pwd\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"" + instancePlain + + "\",\"sensitive\":true}]"); + WorkflowDefinition workflowDefinition = getProcessDefinition(); + workflowDefinition.setProjectCode(projectCode); + workflowDefinition.setGlobalParams( + "[{\"prop\":\"pwd\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"" + definitionCipher + + "\",\"sensitive\":true}]"); + + doNothing().when(projectService).checkHasProjectWritePermissionThrowException(loginUser, projectCode); + when(processService.findWorkflowInstanceDetailById(1)).thenReturn(Optional.of(workflowInstance)); + when(workflowDefinitionDao.queryByCode(46L)).thenReturn(Optional.of(workflowDefinition)); + when(workflowInstanceDao.updateById(workflowInstance)).thenReturn(true); + Mockito.doNothing().when(workflowDefinitionService).checkWorkflowNodeList(any(), any()); + Mockito.doNothing().when(taskDatasourcePermissionChecker).checkPermission(any(), any()); + when(processService.saveTaskRelation(any(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyInt(), any(), + any(), any())).thenReturn(Constants.EXIT_CODE_SUCCESS); + when(processService.saveTaskDefine(any(), Mockito.anyLong(), any(), eq(syncDefine))).thenReturn(1); + when(processService.saveWorkflowDefine(any(), eq(workflowDefinition), eq(syncDefine), eq(Boolean.FALSE))) + .thenReturn(1); + + String submittedGlobalParams = + "[{\"prop\":\"pwd\",\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"******\",\"sensitive\":true}]"; + + workflowInstanceService.updateWorkflowInstance(loginUser, projectCode, 1, + taskRelationJson, taskDefinitionJson, "2020-02-21 00:00:00", syncDefine, + submittedGlobalParams, "", 0); + + List instanceGlobals = + GlobalParameterUtils.deserializeGlobalParameter(workflowInstance.getGlobalParams()); + Assertions.assertEquals(instancePlain, findProperty(instanceGlobals, "pwd").getValue()); + + List definitionGlobals = + GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()); + Property definitionPwd = findProperty(definitionGlobals, "pwd"); + Assertions.assertNotEquals(definitionCipher, definitionPwd.getValue()); + Assertions.assertNotEquals(instancePlain, definitionPwd.getValue()); + Assertions.assertEquals(instancePlain, PasswordUtils.decodePassword(definitionPwd.getValue())); + Assertions.assertFalse(workflowDefinition.getGlobalParams().contains(instancePlain)); + Assertions.assertFalse(workflowDefinition.getGlobalParams().contains("definition-secret-A")); + } + private static Property findProperty(List properties, String prop) { return properties.stream() .filter(property -> prop.equals(property.getProp())) diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java index c7ad4176ca3b..b2ba30580e5d 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtilsTest.java @@ -286,10 +286,10 @@ void maskErrorCommandMasksSensitiveCommandParam() { } @Test - void encodeForCreateEncryptsWhenEnabled() { + void encodeForPersistEncryptsWhenEnabled() { try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { mockEncryptionEnabled(mocked); - List encoded = SensitivePropertyUtils.encodeForCreate( + List encoded = SensitivePropertyUtils.encodeForPersist( Collections.singletonList(sensitive("pwd", "Secret123"))); Assertions.assertNotEquals("Secret123", encoded.get(0).getValue()); Assertions.assertEquals("Secret123", PasswordUtils.decodePassword(encoded.get(0).getValue())); @@ -297,34 +297,37 @@ void encodeForCreateEncryptsWhenEnabled() { } @Test - void encodeForCreateSkipsWhenDisabled() { + void encodeForPersistSkipsWhenDisabled() { try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { mockEncryptionDisabled(mocked); - List encoded = SensitivePropertyUtils.encodeForCreate( + List encoded = SensitivePropertyUtils.encodeForPersist( Collections.singletonList(sensitive("pwd", "Secret123"))); Assertions.assertEquals("Secret123", encoded.get(0).getValue()); } } @Test - void mergeAndEncodeDoesNotDoubleEncryptKeepOriginal() { + void mergeThenEncodeKeepOriginalIsDeterministic() { try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { mockEncryptionEnabled(mocked); String ciphertext = PasswordUtils.encodePassword("Secret123"); - List encoded = SensitivePropertyUtils.mergeAndEncode( + List existingRaw = Collections.singletonList(sensitive("pwd", ciphertext)); + List merged = SensitivePropertyUtils.merge( Collections.singletonList(sensitive("pwd", TaskConstants.SENSITIVE_DATA_MASK)), - Collections.singletonList(sensitive("pwd", ciphertext))); + SensitivePropertyUtils.decodeAfterLoad(existingRaw)); + List encoded = SensitivePropertyUtils.encodeForPersist(merged); Assertions.assertEquals(ciphertext, encoded.get(0).getValue()); } } @Test - void mergeAndEncodeFalseToTrueWithPlaceholderThenEncrypts() { + void mergeThenEncodeFalseToTrueWithPlaceholderThenEncrypts() { try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { mockEncryptionEnabled(mocked); - List encoded = SensitivePropertyUtils.mergeAndEncode( + List merged = SensitivePropertyUtils.merge( Collections.singletonList(sensitive("pwd", TaskConstants.SENSITIVE_DATA_MASK)), Collections.singletonList(nonSensitive("pwd", "plain"))); + List encoded = SensitivePropertyUtils.encodeForPersist(merged); Assertions.assertNotEquals("plain", encoded.get(0).getValue()); Assertions.assertEquals("plain", PasswordUtils.decodePassword(encoded.get(0).getValue())); Assertions.assertTrue(encoded.get(0).isSensitive()); @@ -332,26 +335,30 @@ void mergeAndEncodeFalseToTrueWithPlaceholderThenEncrypts() { } @Test - void mergeAndEncodeTrueToFalseDecodesWhenCiphertextResubmitted() { + void normalizeResubmittedCiphertextTrueToFalse() { try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { mockEncryptionEnabled(mocked); String ciphertext = PasswordUtils.encodePassword("Secret123"); - List encoded = SensitivePropertyUtils.mergeAndEncode( + List existingRaw = Collections.singletonList(sensitive("pwd", ciphertext)); + List merged = SensitivePropertyUtils.merge( Collections.singletonList(nonSensitive("pwd", ciphertext)), - Collections.singletonList(sensitive("pwd", ciphertext))); - Assertions.assertEquals("Secret123", encoded.get(0).getValue()); - Assertions.assertFalse(encoded.get(0).isSensitive()); + SensitivePropertyUtils.decodeAfterLoad(existingRaw)); + List normalized = + SensitivePropertyUtils.normalizeResubmittedCiphertext(merged, existingRaw); + Assertions.assertEquals("Secret123", normalized.get(0).getValue()); + Assertions.assertFalse(normalized.get(0).isSensitive()); } } @Test - void restoreStartParamsDecryptsDefinitionGlobals() { + void restoreStartParamsUsesDecodedDefinitionGlobals() { try (MockedStatic mocked = Mockito.mockStatic(PropertyUtils.class)) { mockEncryptionEnabled(mocked); String ciphertext = PasswordUtils.encodePassword("Secret123"); List restored = SensitivePropertyUtils.restoreStartParams( Collections.singletonList(sensitive("pwd", TaskConstants.SENSITIVE_DATA_MASK)), - Collections.singletonList(sensitive("pwd", ciphertext))); + SensitivePropertyUtils.decodeAfterLoad( + Collections.singletonList(sensitive("pwd", ciphertext)))); Assertions.assertEquals("Secret123", restored.get(0).getValue()); Assertions.assertTrue(restored.get(0).isSensitive()); } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/AbstractCommandHandler.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/AbstractCommandHandler.java index 6af21609948c..a573dbadf84d 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/AbstractCommandHandler.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/AbstractCommandHandler.java @@ -39,6 +39,7 @@ import org.apache.dolphinscheduler.server.master.engine.workflow.listener.IWorkflowLifecycleListener; import org.apache.dolphinscheduler.server.master.runner.WorkflowExecuteContext; import org.apache.dolphinscheduler.server.master.runner.WorkflowExecuteContext.WorkflowExecuteContextBuilder; +import org.apache.dolphinscheduler.server.master.utils.SensitivePropertyCryptoUtils; import org.apache.commons.collections4.CollectionUtils; @@ -112,6 +113,9 @@ protected void assembleWorkflowDefinition( checkArgument(workflowDefinition != null, "Cannot find the WorkflowDefinition: [" + workflowDefinitionCode + ":" + workflowDefinitionVersion + "]"); + // Load boundary: decrypt definition globals once; engine logic uses plaintext. + workflowDefinition.setGlobalParams( + SensitivePropertyCryptoUtils.decodeGlobalParams(workflowDefinition.getGlobalParams())); workflowExecuteContextBuilder.setWorkflowDefinition(workflowDefinition); } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java index e149d4387888..ecf7facd6df5 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java @@ -35,8 +35,6 @@ import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecution; import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecutionBuilder; import org.apache.dolphinscheduler.server.master.runner.WorkflowExecuteContext.WorkflowExecuteContextBuilder; -import org.apache.dolphinscheduler.server.master.utils.SensitivePropertyCryptoUtils; - import org.apache.commons.collections4.CollectionUtils; import java.util.HashMap; @@ -125,9 +123,9 @@ private String mergeCommandParamsWithWorkflowParams(final Command command, Optional.ofNullable(JSONUtils.parseObject(command.getCommandParam(), ICommandParam.class)) .map(ICommandParam::getCommandParams) .orElse(null); + // Definition globals already decrypted at load boundary (assembleWorkflowDefinition). final List globalParamsList = - SensitivePropertyCryptoUtils.decodeSensitiveValues( - GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams())); + GlobalParameterUtils.deserializeGlobalParameter(workflowDefinition.getGlobalParams()); Map finalParams = new HashMap<>(); if (CollectionUtils.isNotEmpty(globalParamsList)) { globalParamsList.forEach(globalParam -> finalParams.put(globalParam.getProp(), globalParam)); diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/graph/WorkflowGraphFactory.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/graph/WorkflowGraphFactory.java index f55df4bc0232..fc8c5caba19a 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/graph/WorkflowGraphFactory.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/graph/WorkflowGraphFactory.java @@ -47,6 +47,8 @@ public IWorkflowGraph createWorkflowGraph(WorkflowDefinition workflowDefinition) workflowDefinition.getCode(), workflowDefinition.getVersion()); + // Keep taskParams ciphertext here: AbstractTaskInstanceFactory copies them into task_instance. + // Decrypt only when building TaskExecutionContext for the worker. List taskDefinitions = taskDefinitionLogDao.queryTaskDefineLogList(workflowTaskRelations) .stream() .map(TaskDefinition.class::cast) diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java index 6338ba481e78..2b8590664774 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/execution/TaskExecutionContextBuilder.java @@ -82,6 +82,7 @@ public TaskExecutionContextBuilder buildTaskDefinitionRelatedInfo(final TaskDefi (int) Math.min(TimeUnit.MINUTES.toSeconds(taskDefinition.getTimeout()), Integer.MAX_VALUE)); } } + // Load/outbound boundary: decrypt for worker context; graph/task_instance keep ciphertext. taskExecutionContext.setTaskParams( SensitivePropertyCryptoUtils.decodeLocalParamsInTaskParams(taskDefinition.getTaskParams())); return this; diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java index c4f23d375a32..ca5abad32481 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/SensitivePropertyCryptoUtils.java @@ -19,15 +19,20 @@ import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; import org.apache.dolphinscheduler.plugin.task.api.model.Property; +import org.apache.dolphinscheduler.plugin.task.api.utils.GlobalParameterUtils; import org.apache.dolphinscheduler.plugin.task.api.utils.PropertySensitiveUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + import java.util.List; import lombok.experimental.UtilityClass; /** - * Decrypt definition-time sensitive values for execution. - * API/UI still mask; Worker receives plaintext copies only. + * Load-boundary decrypt for definition-time sensitive values at execution. + * Call immediately after loading workflow/task definition rows into the run context; + * subsequent engine logic stays on plaintext. Worker receives plaintext copies only. */ @UtilityClass public class SensitivePropertyCryptoUtils { @@ -40,4 +45,16 @@ public String decodeLocalParamsInTaskParams(String taskParams) { return PropertySensitiveUtils.transformLocalParamsInTaskParams(taskParams, SensitivePropertyCryptoUtils::decodeSensitiveValues); } + + /** Load-boundary helper for workflow definition {@code globalParams} JSON. */ + public String decodeGlobalParams(String globalParams) { + if (StringUtils.isEmpty(globalParams)) { + return globalParams; + } + List properties = GlobalParameterUtils.deserializeGlobalParameter(globalParams); + if (CollectionUtils.isEmpty(properties)) { + return globalParams; + } + return GlobalParameterUtils.serializeGlobalParameter(decodeSensitiveValues(properties)); + } } From e761b99449ffc509d93a52c296a45aa6ada4078a Mon Sep 17 00:00:00 2001 From: det101 <3656843+det101@users.noreply.github.com> Date: Mon, 2 Nov 2026 10:10:47 +0800 Subject: [PATCH 5/7] [Chore] Fix spotless check failure in RunWorkflowCommandHandler Add the missing blank line between the org.apache.dolphinscheduler.* and org.apache.commons.* import groups. Without it ./mvnw spotless:check fails, which is the first step of the Sanity Check in every CI workflow, cascading into red Backend / Unit Test / E2E / API-Test / Frontend / Docs jobs. Verified locally: ./mvnw spotless:check passes on the whole reactor. --- .../master/engine/command/handler/RunWorkflowCommandHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java index ecf7facd6df5..56abe26d9db3 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/command/handler/RunWorkflowCommandHandler.java @@ -35,6 +35,7 @@ import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecution; import org.apache.dolphinscheduler.server.master.engine.task.execution.TaskExecutionBuilder; import org.apache.dolphinscheduler.server.master.runner.WorkflowExecuteContext.WorkflowExecuteContextBuilder; + import org.apache.commons.collections4.CollectionUtils; import java.util.HashMap; From 3a9aaa0bde53346d8fbedaa5559b19e71041d984 Mon Sep 17 00:00:00 2001 From: det101 <3656843+det101@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:25:30 +0800 Subject: [PATCH 6/7] [Chore] Retrigger CI From 12b6c4413f7b15da3d27010299942ad694f8fc15 Mon Sep 17 00:00:00 2001 From: det101 <3656843+det101@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:30:40 +0800 Subject: [PATCH 7/7] [Chore] Retrigger CI