github-advanced-security[bot] commented on code in PR #18585: URL: https://github.com/apache/dolphinscheduler/pull/18585#discussion_r4056350348
########## dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java: ########## @@ -0,0 +1,367 @@ +/* + * 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.api.utils; + +import static org.apache.dolphinscheduler.common.constants.Constants.LOCAL_PARAMS; +import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.LOCAL_PARAMS_LIST; + +import org.apache.dolphinscheduler.api.enums.Status; +import org.apache.dolphinscheduler.api.exceptions.ServiceException; +import org.apache.dolphinscheduler.api.vo.TaskDefinitionVO; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.dao.entity.AbstractTaskInstanceContext; +import org.apache.dolphinscheduler.dao.entity.DagData; +import org.apache.dolphinscheduler.dao.entity.TaskDefinition; +import org.apache.dolphinscheduler.dao.entity.TaskDefinitionLog; +import org.apache.dolphinscheduler.dao.entity.TaskInstance; +import org.apache.dolphinscheduler.dao.entity.TaskInstanceDependentDetails; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinition; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinitionLog; +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.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.dolphinscheduler.plugin.task.api.utils.VarPoolUtils; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.experimental.UtilityClass; + +import org.springframework.beans.BeanUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * 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. + */ +@UtilityClass +public class SensitivePropertyUtils { + + /** Create: {@code ******} is never a real value. */ + public void requireNoPlaceholder(List<Property> properties) { + String invalidProp = PropertySensitiveUtils.findPlaceholderProp(properties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating; please re-enter the value"); + } + } + + /** + * Update: restore DB plaintext for keep-original {@code ******}. + * Empty / null is a real empty value. {@code false→true} + {@code ******} is allowed; + * {@code true→false} + {@code ******} is rejected. + */ + public List<Property> merge(List<Property> submittedProperties, List<Property> existingProperties) { + if (CollectionUtils.isEmpty(submittedProperties)) { + return submittedProperties; + } + String invalidProp = PropertySensitiveUtils.findInvalidSensitivePlaceholderProp( + submittedProperties, existingProperties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating, enabling, or disabling sensitive; please re-enter the value"); + } + return PropertySensitiveUtils.mergeSensitiveValuePlaceholders(submittedProperties, existingProperties); + } + + /** Start: {@code ******} is replaced with the matching definition global plaintext. */ + public List<Property> restoreStartParams(List<Property> startParams, List<Property> globalParams) { + if (CollectionUtils.isEmpty(startParams)) { + return startParams; + } + Map<String, Property> globals = CollectionUtils.emptyIfNull(globalParams).stream() + .filter(Objects::nonNull) + .filter(property -> property.getProp() != null) + .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); + List<Property> restored = new ArrayList<>(); + for (Property startParam : startParams) { + if (startParam == null) { + continue; + } + if (!PropertySensitiveUtils.isSensitiveValuePlaceholder(startParam.getValue())) { + restored.add(startParam); + continue; + } + Property global = globals.get(startParam.getProp()); + if (global != null) { + restored.add(PropertySensitiveUtils.copy(global)); + } + } + return restored; + } + + public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { + return rewriteLocalParams(submittedTaskParams, submitted -> { + if (StringUtils.isEmpty(existingTaskParams)) { + requireNoPlaceholder(submitted); + return submitted; + } + return merge(submitted, getLocalParams(existingTaskParams)); + }); + } + + /** + * 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). + */ + public Map<String, Map<String, Object>> mask(Map<String, Map<String, Object>> source) { + if (source == null) { + return null; + } + return maskLocalParamsMap(source); + } + + public DagData mask(DagData source) { + if (source == null) { + return null; + } + DagData copy = new DagData(); + BeanUtils.copyProperties(source, copy); + copy.setWorkflowDefinition(mask(source.getWorkflowDefinition())); + if (source.getTaskDefinitionList() != null) { + copy.setTaskDefinitionList(source.getTaskDefinitionList().stream() + .map(SensitivePropertyUtils::mask) + .collect(Collectors.toList())); + } + return copy; + } + + public WorkflowInstance mask(WorkflowInstance source) { + if (source == null) { + return null; + } + WorkflowInstance copy = new WorkflowInstance(); + BeanUtils.copyProperties(source, copy); + copy.setGlobalParams(maskGlobalParams(copy.getGlobalParams())); + copy.setVarPool(maskVarPool(copy.getVarPool())); + copy.setCommandParam(maskCommandParam(copy.getCommandParam())); + copy.setDagData(mask(source.getDagData())); + return copy; + } + + public WorkflowDefinition mask(WorkflowDefinition source) { + if (source == null) { + return null; + } + if (source instanceof WorkflowDefinitionLog) { + return mask((WorkflowDefinitionLog) source); + } + WorkflowDefinition copy = new WorkflowDefinition(); + BeanUtils.copyProperties(source, copy); + applyWorkflowDefinitionMask(copy); + return copy; + } + + public WorkflowDefinitionLog mask(WorkflowDefinitionLog source) { + if (source == null) { + return null; + } + WorkflowDefinitionLog copy = new WorkflowDefinitionLog(); + BeanUtils.copyProperties(source, copy); + applyWorkflowDefinitionMask(copy); + return copy; + } + + public TaskDefinition mask(TaskDefinition source) { + if (source == null) { + return null; + } + if (source instanceof TaskDefinitionVO) { + return mask((TaskDefinitionVO) source); + } + if (source instanceof TaskDefinitionLog) { + return mask((TaskDefinitionLog) source); + } + TaskDefinition copy = new TaskDefinition(); + BeanUtils.copyProperties(source, copy); + applyTaskDefinitionMask(copy); + return copy; + } + + public TaskDefinitionLog mask(TaskDefinitionLog source) { + if (source == null) { + return null; + } + TaskDefinitionLog copy = new TaskDefinitionLog(); + BeanUtils.copyProperties(source, copy); + applyTaskDefinitionMask(copy); + return copy; + } + + public TaskDefinitionVO mask(TaskDefinitionVO source) { Review Comment: ## CodeQL / Confusing overloading of methods Method SensitivePropertyUtils.mask(..) could be confused with overloaded method [mask](1), since dispatch depends on static types. [Show more details](https://github.com/apache/dolphinscheduler/security/code-scanning/5860) ########## dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java: ########## @@ -0,0 +1,367 @@ +/* + * 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.api.utils; + +import static org.apache.dolphinscheduler.common.constants.Constants.LOCAL_PARAMS; +import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.LOCAL_PARAMS_LIST; + +import org.apache.dolphinscheduler.api.enums.Status; +import org.apache.dolphinscheduler.api.exceptions.ServiceException; +import org.apache.dolphinscheduler.api.vo.TaskDefinitionVO; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.dao.entity.AbstractTaskInstanceContext; +import org.apache.dolphinscheduler.dao.entity.DagData; +import org.apache.dolphinscheduler.dao.entity.TaskDefinition; +import org.apache.dolphinscheduler.dao.entity.TaskDefinitionLog; +import org.apache.dolphinscheduler.dao.entity.TaskInstance; +import org.apache.dolphinscheduler.dao.entity.TaskInstanceDependentDetails; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinition; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinitionLog; +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.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.dolphinscheduler.plugin.task.api.utils.VarPoolUtils; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.experimental.UtilityClass; + +import org.springframework.beans.BeanUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * 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. + */ +@UtilityClass +public class SensitivePropertyUtils { + + /** Create: {@code ******} is never a real value. */ + public void requireNoPlaceholder(List<Property> properties) { + String invalidProp = PropertySensitiveUtils.findPlaceholderProp(properties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating; please re-enter the value"); + } + } + + /** + * Update: restore DB plaintext for keep-original {@code ******}. + * Empty / null is a real empty value. {@code false→true} + {@code ******} is allowed; + * {@code true→false} + {@code ******} is rejected. + */ + public List<Property> merge(List<Property> submittedProperties, List<Property> existingProperties) { + if (CollectionUtils.isEmpty(submittedProperties)) { + return submittedProperties; + } + String invalidProp = PropertySensitiveUtils.findInvalidSensitivePlaceholderProp( + submittedProperties, existingProperties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating, enabling, or disabling sensitive; please re-enter the value"); + } + return PropertySensitiveUtils.mergeSensitiveValuePlaceholders(submittedProperties, existingProperties); + } + + /** Start: {@code ******} is replaced with the matching definition global plaintext. */ + public List<Property> restoreStartParams(List<Property> startParams, List<Property> globalParams) { + if (CollectionUtils.isEmpty(startParams)) { + return startParams; + } + Map<String, Property> globals = CollectionUtils.emptyIfNull(globalParams).stream() + .filter(Objects::nonNull) + .filter(property -> property.getProp() != null) + .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); + List<Property> restored = new ArrayList<>(); + for (Property startParam : startParams) { + if (startParam == null) { + continue; + } + if (!PropertySensitiveUtils.isSensitiveValuePlaceholder(startParam.getValue())) { + restored.add(startParam); + continue; + } + Property global = globals.get(startParam.getProp()); + if (global != null) { + restored.add(PropertySensitiveUtils.copy(global)); + } + } + return restored; + } + + public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { + return rewriteLocalParams(submittedTaskParams, submitted -> { + if (StringUtils.isEmpty(existingTaskParams)) { + requireNoPlaceholder(submitted); + return submitted; + } + return merge(submitted, getLocalParams(existingTaskParams)); + }); + } + + /** + * 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). + */ + public Map<String, Map<String, Object>> mask(Map<String, Map<String, Object>> source) { + if (source == null) { + return null; + } + return maskLocalParamsMap(source); + } + + public DagData mask(DagData source) { + if (source == null) { + return null; + } + DagData copy = new DagData(); + BeanUtils.copyProperties(source, copy); + copy.setWorkflowDefinition(mask(source.getWorkflowDefinition())); + if (source.getTaskDefinitionList() != null) { + copy.setTaskDefinitionList(source.getTaskDefinitionList().stream() + .map(SensitivePropertyUtils::mask) + .collect(Collectors.toList())); + } + return copy; + } + + public WorkflowInstance mask(WorkflowInstance source) { + if (source == null) { + return null; + } + WorkflowInstance copy = new WorkflowInstance(); + BeanUtils.copyProperties(source, copy); + copy.setGlobalParams(maskGlobalParams(copy.getGlobalParams())); + copy.setVarPool(maskVarPool(copy.getVarPool())); + copy.setCommandParam(maskCommandParam(copy.getCommandParam())); + copy.setDagData(mask(source.getDagData())); + return copy; + } + + public WorkflowDefinition mask(WorkflowDefinition source) { Review Comment: ## CodeQL / Confusing overloading of methods Method SensitivePropertyUtils.mask(..) could be confused with overloaded method [mask](1), since dispatch depends on static types. [Show more details](https://github.com/apache/dolphinscheduler/security/code-scanning/5862) ########## dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java: ########## @@ -0,0 +1,367 @@ +/* + * 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.api.utils; + +import static org.apache.dolphinscheduler.common.constants.Constants.LOCAL_PARAMS; +import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.LOCAL_PARAMS_LIST; + +import org.apache.dolphinscheduler.api.enums.Status; +import org.apache.dolphinscheduler.api.exceptions.ServiceException; +import org.apache.dolphinscheduler.api.vo.TaskDefinitionVO; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.dao.entity.AbstractTaskInstanceContext; +import org.apache.dolphinscheduler.dao.entity.DagData; +import org.apache.dolphinscheduler.dao.entity.TaskDefinition; +import org.apache.dolphinscheduler.dao.entity.TaskDefinitionLog; +import org.apache.dolphinscheduler.dao.entity.TaskInstance; +import org.apache.dolphinscheduler.dao.entity.TaskInstanceDependentDetails; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinition; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinitionLog; +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.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.dolphinscheduler.plugin.task.api.utils.VarPoolUtils; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.experimental.UtilityClass; + +import org.springframework.beans.BeanUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * 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. + */ +@UtilityClass +public class SensitivePropertyUtils { + + /** Create: {@code ******} is never a real value. */ + public void requireNoPlaceholder(List<Property> properties) { + String invalidProp = PropertySensitiveUtils.findPlaceholderProp(properties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating; please re-enter the value"); + } + } + + /** + * Update: restore DB plaintext for keep-original {@code ******}. + * Empty / null is a real empty value. {@code false→true} + {@code ******} is allowed; + * {@code true→false} + {@code ******} is rejected. + */ + public List<Property> merge(List<Property> submittedProperties, List<Property> existingProperties) { + if (CollectionUtils.isEmpty(submittedProperties)) { + return submittedProperties; + } + String invalidProp = PropertySensitiveUtils.findInvalidSensitivePlaceholderProp( + submittedProperties, existingProperties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating, enabling, or disabling sensitive; please re-enter the value"); + } + return PropertySensitiveUtils.mergeSensitiveValuePlaceholders(submittedProperties, existingProperties); + } + + /** Start: {@code ******} is replaced with the matching definition global plaintext. */ + public List<Property> restoreStartParams(List<Property> startParams, List<Property> globalParams) { + if (CollectionUtils.isEmpty(startParams)) { + return startParams; + } + Map<String, Property> globals = CollectionUtils.emptyIfNull(globalParams).stream() + .filter(Objects::nonNull) + .filter(property -> property.getProp() != null) + .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); + List<Property> restored = new ArrayList<>(); + for (Property startParam : startParams) { + if (startParam == null) { + continue; + } + if (!PropertySensitiveUtils.isSensitiveValuePlaceholder(startParam.getValue())) { + restored.add(startParam); + continue; + } + Property global = globals.get(startParam.getProp()); + if (global != null) { + restored.add(PropertySensitiveUtils.copy(global)); + } + } + return restored; + } + + public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { + return rewriteLocalParams(submittedTaskParams, submitted -> { + if (StringUtils.isEmpty(existingTaskParams)) { + requireNoPlaceholder(submitted); + return submitted; + } + return merge(submitted, getLocalParams(existingTaskParams)); + }); + } + + /** + * 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). + */ + public Map<String, Map<String, Object>> mask(Map<String, Map<String, Object>> source) { + if (source == null) { + return null; + } + return maskLocalParamsMap(source); + } + + public DagData mask(DagData source) { + if (source == null) { + return null; + } + DagData copy = new DagData(); + BeanUtils.copyProperties(source, copy); + copy.setWorkflowDefinition(mask(source.getWorkflowDefinition())); + if (source.getTaskDefinitionList() != null) { + copy.setTaskDefinitionList(source.getTaskDefinitionList().stream() + .map(SensitivePropertyUtils::mask) + .collect(Collectors.toList())); + } + return copy; + } + + public WorkflowInstance mask(WorkflowInstance source) { + if (source == null) { + return null; + } + WorkflowInstance copy = new WorkflowInstance(); + BeanUtils.copyProperties(source, copy); + copy.setGlobalParams(maskGlobalParams(copy.getGlobalParams())); + copy.setVarPool(maskVarPool(copy.getVarPool())); + copy.setCommandParam(maskCommandParam(copy.getCommandParam())); + copy.setDagData(mask(source.getDagData())); + return copy; + } + + public WorkflowDefinition mask(WorkflowDefinition source) { + if (source == null) { + return null; + } + if (source instanceof WorkflowDefinitionLog) { + return mask((WorkflowDefinitionLog) source); + } + WorkflowDefinition copy = new WorkflowDefinition(); + BeanUtils.copyProperties(source, copy); + applyWorkflowDefinitionMask(copy); + return copy; + } + + public WorkflowDefinitionLog mask(WorkflowDefinitionLog source) { + if (source == null) { + return null; + } + WorkflowDefinitionLog copy = new WorkflowDefinitionLog(); + BeanUtils.copyProperties(source, copy); + applyWorkflowDefinitionMask(copy); + return copy; + } + + public TaskDefinition mask(TaskDefinition source) { + if (source == null) { + return null; + } + if (source instanceof TaskDefinitionVO) { + return mask((TaskDefinitionVO) source); + } + if (source instanceof TaskDefinitionLog) { + return mask((TaskDefinitionLog) source); + } + TaskDefinition copy = new TaskDefinition(); + BeanUtils.copyProperties(source, copy); + applyTaskDefinitionMask(copy); + return copy; + } + + public TaskDefinitionLog mask(TaskDefinitionLog source) { + if (source == null) { + return null; + } + TaskDefinitionLog copy = new TaskDefinitionLog(); + BeanUtils.copyProperties(source, copy); + applyTaskDefinitionMask(copy); + return copy; + } + + public TaskDefinitionVO mask(TaskDefinitionVO source) { + if (source == null) { + return null; + } + TaskDefinitionVO copy = new TaskDefinitionVO(); + BeanUtils.copyProperties(source, copy); + applyTaskDefinitionMask(copy); + return copy; + } + + public TaskInstance mask(TaskInstance source) { Review Comment: ## CodeQL / Confusing overloading of methods Method SensitivePropertyUtils.mask(..) could be confused with overloaded method [mask](1), since dispatch depends on static types. [Show more details](https://github.com/apache/dolphinscheduler/security/code-scanning/5859) ########## dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/SensitivePropertyUtils.java: ########## @@ -0,0 +1,367 @@ +/* + * 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.api.utils; + +import static org.apache.dolphinscheduler.common.constants.Constants.LOCAL_PARAMS; +import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.LOCAL_PARAMS_LIST; + +import org.apache.dolphinscheduler.api.enums.Status; +import org.apache.dolphinscheduler.api.exceptions.ServiceException; +import org.apache.dolphinscheduler.api.vo.TaskDefinitionVO; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.dao.entity.AbstractTaskInstanceContext; +import org.apache.dolphinscheduler.dao.entity.DagData; +import org.apache.dolphinscheduler.dao.entity.TaskDefinition; +import org.apache.dolphinscheduler.dao.entity.TaskDefinitionLog; +import org.apache.dolphinscheduler.dao.entity.TaskInstance; +import org.apache.dolphinscheduler.dao.entity.TaskInstanceDependentDetails; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinition; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinitionLog; +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.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.dolphinscheduler.plugin.task.api.utils.VarPoolUtils; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.experimental.UtilityClass; + +import org.springframework.beans.BeanUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * 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. + */ +@UtilityClass +public class SensitivePropertyUtils { + + /** Create: {@code ******} is never a real value. */ + public void requireNoPlaceholder(List<Property> properties) { + String invalidProp = PropertySensitiveUtils.findPlaceholderProp(properties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating; please re-enter the value"); + } + } + + /** + * Update: restore DB plaintext for keep-original {@code ******}. + * Empty / null is a real empty value. {@code false→true} + {@code ******} is allowed; + * {@code true→false} + {@code ******} is rejected. + */ + public List<Property> merge(List<Property> submittedProperties, List<Property> existingProperties) { + if (CollectionUtils.isEmpty(submittedProperties)) { + return submittedProperties; + } + String invalidProp = PropertySensitiveUtils.findInvalidSensitivePlaceholderProp( + submittedProperties, existingProperties); + if (invalidProp != null) { + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, + "parameter '" + invalidProp + + "' cannot use ****** when creating, enabling, or disabling sensitive; please re-enter the value"); + } + return PropertySensitiveUtils.mergeSensitiveValuePlaceholders(submittedProperties, existingProperties); + } + + /** Start: {@code ******} is replaced with the matching definition global plaintext. */ + public List<Property> restoreStartParams(List<Property> startParams, List<Property> globalParams) { + if (CollectionUtils.isEmpty(startParams)) { + return startParams; + } + Map<String, Property> globals = CollectionUtils.emptyIfNull(globalParams).stream() + .filter(Objects::nonNull) + .filter(property -> property.getProp() != null) + .collect(Collectors.toMap(Property::getProp, Function.identity(), (left, right) -> right)); + List<Property> restored = new ArrayList<>(); + for (Property startParam : startParams) { + if (startParam == null) { + continue; + } + if (!PropertySensitiveUtils.isSensitiveValuePlaceholder(startParam.getValue())) { + restored.add(startParam); + continue; + } + Property global = globals.get(startParam.getProp()); + if (global != null) { + restored.add(PropertySensitiveUtils.copy(global)); + } + } + return restored; + } + + public String mergeLocalParams(String submittedTaskParams, String existingTaskParams) { + return rewriteLocalParams(submittedTaskParams, submitted -> { + if (StringUtils.isEmpty(existingTaskParams)) { + requireNoPlaceholder(submitted); + return submitted; + } + return merge(submitted, getLocalParams(existingTaskParams)); + }); + } + + /** + * 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). + */ + public Map<String, Map<String, Object>> mask(Map<String, Map<String, Object>> source) { + if (source == null) { + return null; + } + return maskLocalParamsMap(source); + } + + public DagData mask(DagData source) { + if (source == null) { + return null; + } + DagData copy = new DagData(); + BeanUtils.copyProperties(source, copy); + copy.setWorkflowDefinition(mask(source.getWorkflowDefinition())); + if (source.getTaskDefinitionList() != null) { + copy.setTaskDefinitionList(source.getTaskDefinitionList().stream() + .map(SensitivePropertyUtils::mask) + .collect(Collectors.toList())); + } + return copy; + } + + public WorkflowInstance mask(WorkflowInstance source) { + if (source == null) { + return null; + } + WorkflowInstance copy = new WorkflowInstance(); + BeanUtils.copyProperties(source, copy); + copy.setGlobalParams(maskGlobalParams(copy.getGlobalParams())); + copy.setVarPool(maskVarPool(copy.getVarPool())); + copy.setCommandParam(maskCommandParam(copy.getCommandParam())); + copy.setDagData(mask(source.getDagData())); + return copy; + } + + public WorkflowDefinition mask(WorkflowDefinition source) { + if (source == null) { + return null; + } + if (source instanceof WorkflowDefinitionLog) { + return mask((WorkflowDefinitionLog) source); + } + WorkflowDefinition copy = new WorkflowDefinition(); + BeanUtils.copyProperties(source, copy); + applyWorkflowDefinitionMask(copy); + return copy; + } + + public WorkflowDefinitionLog mask(WorkflowDefinitionLog source) { + if (source == null) { + return null; + } + WorkflowDefinitionLog copy = new WorkflowDefinitionLog(); + BeanUtils.copyProperties(source, copy); + applyWorkflowDefinitionMask(copy); + return copy; + } + + public TaskDefinition mask(TaskDefinition source) { Review Comment: ## CodeQL / Confusing overloading of methods Method SensitivePropertyUtils.mask(..) could be confused with overloaded method [mask](1), since dispatch depends on static types. [Show more details](https://github.com/apache/dolphinscheduler/security/code-scanning/5861) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
