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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.zstack.header.storage.addon.primary;

/**
* Interface for external primary storage config desensitization.
*
* Each external primary storage plugin should implement this on its Config class.
* ZStack will call {@link #desensitize()} before outputting config to API responses or logs,
* so plugin developers only need to implement this method to mask sensitive fields.
*/
public interface ExternalPrimaryStorageConfig {
/**
* The identity of the external primary storage plugin (e.g. "zbs").
*/
String getIdentity();

/**
* Return a desensitized copy of this config with sensitive fields masked.
* The original object must not be modified.
*/
ExternalPrimaryStorageConfig desensitize();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,36 @@

import org.zstack.header.search.Inventory;
import org.zstack.header.storage.primary.PrimaryStorageInventory;
import org.zstack.utils.BeanUtils;
import org.zstack.utils.Utils;
import org.zstack.utils.gson.JSONObjectUtil;
import org.zstack.utils.logging.CLogger;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Inventory(mappingVOClass = ExternalPrimaryStorageVO.class)
public class ExternalPrimaryStorageInventory extends PrimaryStorageInventory {
private static final CLogger logger = Utils.getLogger(ExternalPrimaryStorageInventory.class);
private static final Map<String, Class<? extends ExternalPrimaryStorageConfig>> configClassRegistry = new ConcurrentHashMap<>();

static {
for (Class<? extends ExternalPrimaryStorageConfig> clz : BeanUtils.reflections.getSubTypesOf(ExternalPrimaryStorageConfig.class)) {
if (clz.isInterface()) {
continue;
}
try {
ExternalPrimaryStorageConfig instance = clz.newInstance();
configClassRegistry.put(instance.getIdentity(), clz);
} catch (Exception e) {
logger.warn(String.format("failed to register ExternalPrimaryStorageConfig: %s", clz.getName()), e);
}
}
}

private String identity;

/**
Expand Down Expand Up @@ -60,44 +80,22 @@ public ExternalPrimaryStorageInventory() {
public ExternalPrimaryStorageInventory(ExternalPrimaryStorageVO lvo) {
super(lvo);
identity = lvo.getIdentity();
config = JSONObjectUtil.toObject(lvo.getConfig(), LinkedHashMap.class);
desensitizeConfig(config);
addonInfo = JSONObjectUtil.toObject(lvo.getAddonInfo(), LinkedHashMap.class);
outputProtocols = lvo.getOutputProtocols().stream().map(PrimaryStorageOutputProtocolRefVO::getOutputProtocol).collect(Collectors.toList());
defaultProtocol = lvo.getDefaultProtocol();
}

public static ExternalPrimaryStorageInventory valueOf(ExternalPrimaryStorageVO lvo) {
return new ExternalPrimaryStorageInventory(lvo);
}

private static void desensitizeConfig(Map config) {
if (config == null) return;
desensitizeUrlList(config, "mdsUrls");
desensitizeUrlList(config, "mdsInfos");
}

private static void desensitizeUrlList(Map config, String key) {
Object urls = config.get(key);
if (urls instanceof List) {
List<String> desensitized = new ArrayList<>();
for (Object url : (List) urls) {
desensitized.add(desensitizeUrl(String.valueOf(url)));
}
config.put(key, desensitized);
Class<? extends ExternalPrimaryStorageConfig> configClass = configClassRegistry.get(identity);
if (configClass != null) {
ExternalPrimaryStorageConfig typedConfig = JSONObjectUtil.toObject(lvo.getConfig(), configClass);
ExternalPrimaryStorageConfig desensitized = typedConfig.desensitize();
config = JSONObjectUtil.toObject(JSONObjectUtil.toJsonString(desensitized), LinkedHashMap.class);
} else {
config = JSONObjectUtil.toObject(lvo.getConfig(), LinkedHashMap.class);
}
}

private static String desensitizeUrl(String url) {
int atIndex = url.lastIndexOf('@');
if (atIndex > 0) {
int schemeIndex = url.indexOf("://");
if (schemeIndex >= 0 && schemeIndex < atIndex) {
return url.substring(0, schemeIndex + 3) + "***" + url.substring(atIndex);
}
return "***" + url.substring(atIndex);
}
return url;
public static ExternalPrimaryStorageInventory valueOf(ExternalPrimaryStorageVO lvo) {
return new ExternalPrimaryStorageInventory(lvo);
}

public String getIdentity() {
Expand Down
23 changes: 22 additions & 1 deletion plugin/zbs/src/main/java/org/zstack/storage/zbs/Config.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package org.zstack.storage.zbs;

import org.zstack.header.storage.addon.primary.ExternalPrimaryStorageConfig;
import org.zstack.utils.gson.JSONObjectUtil;

import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
* @author Xingwei Yu
* @date 2024/4/2 11:13
*/
public class Config {
public class Config implements ExternalPrimaryStorageConfig {
public static class Pool {
public String logicalName;
public String aliasName;
Expand All @@ -22,11 +25,29 @@ public Pool(String logicalName, String aliasName) {
public Pool() {}
}

private static final Pattern URI_CREDENTIAL_PATTERN = Pattern.compile(":[^:@]*@");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 查找 MdsUri 类的实现,确认密码中特殊字符的处理方式
ast-grep --pattern $'class MdsUri {
  $$$
}'

# 搜索密码编码相关的处理逻辑
rg -n "encode|decode|URLEncoder|URLDecoder" --type java -g "**/zbs/**"

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 搜索 MdsUri 相关的所有文件
find . -type f -name "*MdsUri*" -o -name "*Uri*" | grep -i zbs | head -20

Repository: MatheMatrix/zstack

Length of output: 123


🏁 Script executed:

# 搜索包含 mdsUrl 或 URI 的代码
rg -n "mdsUrl|MdsUri" --type java -A 3 -B 3 | head -100

Repository: MatheMatrix/zstack

Length of output: 9709


🏁 Script executed:

# 查看 Config.java 文件的完整内容
find . -type f -path "**/zbs/**/Config.java" | head -5

Repository: MatheMatrix/zstack

Length of output: 123


🏁 Script executed:

# 搜索正则表达式的使用情况
rg -n "URI_CREDENTIAL_PATTERN" --type java

Repository: MatheMatrix/zstack

Length of output: 367


🏁 Script executed:

# 查看 desensitize 方法的调用情况和上下文
rg -n "desensitize" --type java -A 5 -B 5 | head -80

Repository: MatheMatrix/zstack

Length of output: 7898


🏁 Script executed:

# 查看 MdsUri.java 的完整内容,特别是密码处理
cat -n plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsUri.java | head -150

Repository: MatheMatrix/zstack

Length of output: 5924


🏁 Script executed:

# 查看 Config.java 的完整内容
cat -n plugin/zbs/src/main/java/org/zstack/storage/zbs/Config.java

Repository: MatheMatrix/zstack

Length of output: 2872


🏁 Script executed:

# 搜索是否有任何 URLEncoder 或类似的编码处理
rg -n "URLEncoder|URLDecoder|encode|decode" plugin/zbs/src/main/java/org/zstack/storage/zbs/ -A 3 -B 3

Repository: MatheMatrix/zstack

Length of output: 44


正则表达式无法正确处理密码中包含特殊字符的情况。

MdsUri 的解析逻辑支持密码中包含 :@ 字符(通过 split(":", 2)lastIndexOf("@")),但当前正则 :[^:@]*@ 无法匹配这些情况:

  • user:pass:word@host - 密码含 :,正则无法匹配
  • user:pass@word@host - 只匹配 :pass@,留下 word@host 未掩码

应修改正则或采用其他方式来正确掩码含特殊字符的密码。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/zbs/src/main/java/org/zstack/storage/zbs/Config.java` at line 28,
当前用于掩码 URI 中凭据的正则 URI_CREDENTIAL_PATTERN = Pattern.compile(":[^:@]*@") 无法处理密码中含
':' 或 '@' 的情况;请改为不依赖字符排除的实现:在 Config.java 中移除或替换该正则,改用基于 MdsUri 解析逻辑的显式定位——使用
split(":", 2) 找到用户/密码起始位置并用 lastIndexOf("@")
找到凭据结束位置,然后用这两个索引之间的子串替换为掩码,或将正则改为匹配从第一个 ':' 到最后一个 '@'(非贪婪/明确定位)的模式以确保像
user:pass:word@host 和 user:pass@word@host 这类情况都能正确掩码。


private List<String> mdsUrls;

@Override
public String getIdentity() {
return ZbsConstants.IDENTITY;
}
private List<Pool> pools;
private String logicalPoolName;
private transient List<String> poolNames;

@Override
public ExternalPrimaryStorageConfig desensitize() {
Config copy = JSONObjectUtil.toObject(JSONObjectUtil.toJsonString(this), Config.class);
if (copy.mdsUrls != null) {
copy.mdsUrls = copy.mdsUrls.stream()
.map(url -> URI_CREDENTIAL_PATTERN.matcher(url).replaceFirst(":*****@"))
.collect(Collectors.toList());
}
return copy;
}

public List<String> getMdsUrls() {
return mdsUrls;
}
Expand Down
8 changes: 6 additions & 2 deletions plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsInfo.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.zstack.storage.zbs;

import org.zstack.header.log.NoLogging;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
Expand All @@ -9,9 +12,10 @@
* @author Xingwei Yu
* @date 2024/4/10 23:18
*/
public class MdsInfo {
public class MdsInfo implements Serializable {
private String username;
private String password;
@NoLogging
private transient String password;
private int port = 22;
private String addr;
private String externalAddr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,9 +532,9 @@ class ZbsPrimaryStorageCase extends SubCase {
def addonInfo = Q.New(ExternalPrimaryStorageVO.class).select(ExternalPrimaryStorageVO_.addonInfo).eq(ExternalPrimaryStorageVO_.uuid, ps.uuid).findValue()

assert addonInfo == "{\"clusterInfo\":{\"uuid\":\"123456789\",\"version\":\"1.6.1-for-test\"}," +
"\"mdsInfos\":[{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," +
"{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," +
"{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," +
"\"mdsInfos\":[{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," +
"{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," +
"{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," +
"\"logicalPoolInfos\":[{\"physicalPoolID\":1,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":1,\"usedSize\":322961408,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":968884224,\"physicalPoolName\":\"pool1\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool1\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":3221225472}," +
"{\"physicalPoolID\":2,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":2,\"usedSize\":123456789,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":123456789,\"physicalPoolName\":\"pool2\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool2\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":987654321}]}"
assert data == null
Expand Down Expand Up @@ -566,9 +566,9 @@ class ZbsPrimaryStorageCase extends SubCase {
addonInfo = Q.New(ExternalPrimaryStorageVO.class).select(ExternalPrimaryStorageVO_.addonInfo).eq(ExternalPrimaryStorageVO_.uuid, ps.uuid).findValue()

assert addonInfo == "{\"clusterInfo\":{\"uuid\":\"123456789\",\"version\":\"1.6.1-for-test\"}," +
"\"mdsInfos\":[{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," +
"{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," +
"{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," +
"\"mdsInfos\":[{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," +
"{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," +
"{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," +
"\"logicalPoolInfos\":[{\"physicalPoolID\":1,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":1,\"usedSize\":322961408,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":968884224,\"physicalPoolName\":\"pool1\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool1\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":3221225472}," +
"{\"physicalPoolID\":2,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":2,\"usedSize\":123456789,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":123456789,\"physicalPoolName\":\"pool2\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool2\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":987654321}]}"

Expand Down Expand Up @@ -615,9 +615,9 @@ class ZbsPrimaryStorageCase extends SubCase {
addonInfo = Q.New(ExternalPrimaryStorageVO.class).select(ExternalPrimaryStorageVO_.addonInfo).eq(ExternalPrimaryStorageVO_.uuid, ps.uuid).findValue()

assert addonInfo == "{\"clusterInfo\":{\"uuid\":\"123456789\",\"version\":\"1.6.1-for-test\"}," +
"\"mdsInfos\":[{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," +
"{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," +
"{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}]," +
"\"mdsInfos\":[{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," +
"{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," +
"{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}]," +
"\"logicalPoolInfos\":[{\"physicalPoolID\":1,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":1,\"usedSize\":322961408,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":968884224,\"physicalPoolName\":\"pool1\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool1\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":3221225472}," +
"{\"physicalPoolID\":2,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":2,\"usedSize\":123456789,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":123456789,\"physicalPoolName\":\"pool2\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool2\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":987654321}]}"

Expand Down